-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: added helpers, components item, pill, switch and toggle
- Loading branch information
1 parent
b46074c
commit fa75e02
Showing
66 changed files
with
4,854 additions
and
6 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
# Actions | ||
|
||
This package provides a set of actions that can be used to perform various tasks. | ||
|
||
## Keyboard | ||
|
||
The keyboard action can be used to map keyboard events to actions. The following example shows how to map the `K` key combination to an action: | ||
|
||
The default behavior is to listen to keyup events. | ||
|
||
- [x] Configuration driven | ||
- [x] Custom events can be defined | ||
- [x] Supports mapping an array of keys to an event | ||
- [x] Supports mapping a regex to an event | ||
- [ ] Support key modifiers | ||
- [ ] Support a combination of regex patterns and array of keys | ||
|
||
Default configuration | ||
|
||
- _add_: alphabet keys cause an `add` event | ||
- _submit_: enter causes a `submit` event | ||
- _cancel_: escape causes a `cancel` event | ||
- _delete_: backspace or delete causes a `delete` event | ||
|
||
### Basic Usage | ||
|
||
```svelte | ||
<script> | ||
import { keyboard } from '@fumbl/actions' | ||
function handleKey(event) { | ||
console.log(`${event.detail} pressed`) | ||
} | ||
</script> | ||
<div use:keyboard onadd={handleKey}></div> | ||
``` | ||
|
||
### Custom Events | ||
|
||
```svelte | ||
<script> | ||
import { keyboard } from '@fumbl/actions' | ||
function handleKey(event) { | ||
console.log(`${event.detail} pressed`) | ||
} | ||
const config = { | ||
add: ['a', 'b', 'c'], | ||
submit: 'enter', | ||
cancel: 'escape', | ||
delete: ['backspace', 'delete'] | ||
} | ||
</script> | ||
<div use:keyboard={config} onadd={handleKey}></div> | ||
``` |
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,38 @@ | ||
{ | ||
"name": "@rokkit/actions", | ||
"version": "1.0.0-next.0", | ||
"description": "Contains generic actions that can be used in various components.", | ||
"author": "Jerry Thomas <[email protected]>", | ||
"license": "MIT", | ||
"main": "index.js", | ||
"module": "src/index.js", | ||
"types": "dist/index.d.ts", | ||
"type": "module", | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"scripts": { | ||
"prepublishOnly": "tsc --project tsconfig.build.json", | ||
"clean": "rm -rf dist", | ||
"build": "pnpm clean && pnpm prepublishOnly" | ||
}, | ||
"files": [ | ||
"src/**/*.js", | ||
"src/**/*.svelte" | ||
], | ||
"exports": { | ||
"./src": "./src", | ||
"./package.json": "./package.json", | ||
".": { | ||
"types": "./dist/index.d.ts", | ||
"import": "./src/index.js", | ||
"svelte": "./src/index.js" | ||
} | ||
}, | ||
"dependencies": { | ||
"ramda": "^0.30.1" | ||
}, | ||
"devDependencies": { | ||
"@rokkit/helpers": "workspace:*" | ||
} | ||
} |
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,9 @@ | ||
import { describe, it, expect } from 'vitest' | ||
// skipcq: JS-C1003 - Importing all components for verification | ||
import * as actions from '../src/index.js' | ||
|
||
describe('actions', () => { | ||
it('should contain all exported actions', () => { | ||
expect(Object.keys(actions)).toEqual(['keyboard']) | ||
}) | ||
}) |
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,156 @@ | ||
import { vi, it, expect, describe, beforeEach, afterEach } from 'vitest' | ||
import { flushSync } from 'svelte' | ||
import { keyboard } from '../src/keyboard.svelte.js' | ||
import { toHaveBeenDispatchedWith } from '@rokkit/helpers/matchers' | ||
|
||
expect.extend({ toHaveBeenDispatchedWith }) | ||
|
||
describe('keyboard', () => { | ||
const node = document.createElement('div') | ||
const spies = { | ||
add: vi.fn(), | ||
remove: vi.fn(), | ||
submit: vi.fn() | ||
} | ||
|
||
const keys = ['a', 'b', 'Enter', 'Backspace'] | ||
keys.forEach((key) => { | ||
const button = document.createElement('button') | ||
button.setAttribute('data-key', key) | ||
node.appendChild(button) | ||
}) | ||
|
||
beforeEach(() => { | ||
Object.entries(spies).forEach(([event, callback]) => { | ||
node.addEventListener(event, callback) | ||
}) | ||
}) | ||
|
||
afterEach(() => { | ||
Object.entries(spies).forEach(([event, callback]) => { | ||
node.removeEventListener(event, callback) | ||
}) | ||
vi.resetAllMocks() | ||
}) | ||
|
||
it('should add and remove keyup handlers to document', () => { | ||
const addEventListenerSpy = vi.spyOn(document, 'addEventListener') | ||
const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener') | ||
const removeEventOnNodeSpy = vi.spyOn(node, 'removeEventListener') | ||
const addEventOnNodeSpy = vi.spyOn(node, 'addEventListener') | ||
|
||
const cleanup = $effect.root(() => keyboard(node)) | ||
flushSync() | ||
|
||
expect(addEventListenerSpy).toHaveBeenCalledTimes(1) | ||
expect(addEventListenerSpy).toHaveBeenNthCalledWith(1, 'keyup', expect.any(Function), {}) | ||
expect(addEventOnNodeSpy).toHaveBeenCalledTimes(1) | ||
expect(addEventOnNodeSpy).toHaveBeenNthCalledWith(1, 'click', expect.any(Function), {}) | ||
|
||
cleanup() | ||
expect(removeEventListenerSpy).toHaveBeenCalledTimes(1) | ||
expect(removeEventListenerSpy).toHaveBeenNthCalledWith(1, 'keyup', expect.any(Function), {}) | ||
expect(removeEventOnNodeSpy).toHaveBeenCalledTimes(1) | ||
expect(removeEventOnNodeSpy).toHaveBeenNthCalledWith(1, 'click', expect.any(Function), {}) | ||
|
||
addEventListenerSpy.mockRestore() | ||
removeEventListenerSpy.mockRestore() | ||
removeEventOnNodeSpy.mockRestore() | ||
addEventOnNodeSpy.mockRestore() | ||
}) | ||
|
||
it('should not dispatch any event when an unmapped key is pressed', () => { | ||
const cleanup = $effect.root(() => keyboard(node)) | ||
flushSync() | ||
|
||
document.dispatchEvent(new KeyboardEvent('keyup', { key: '-' })) | ||
expect(spies.add).not.toHaveBeenCalled() | ||
expect(spies.remove).not.toHaveBeenCalled() | ||
expect(spies.submit).not.toHaveBeenCalled() | ||
cleanup() | ||
}) | ||
|
||
it('should dispatch "add" event when an alphabet is pressed', () => { | ||
const cleanup = $effect.root(() => keyboard(node)) | ||
flushSync() | ||
|
||
document.dispatchEvent(new KeyboardEvent('keyup', { key: 'a' })) | ||
expect(spies.add).toHaveBeenDispatchedWith('a') | ||
expect(spies.remove).not.toHaveBeenCalled() | ||
expect(spies.submit).not.toHaveBeenCalled() | ||
|
||
cleanup() | ||
}) | ||
|
||
it('should dispatch "remove" event when delete is pressed', () => { | ||
const cleanup = $effect.root(() => keyboard(node)) | ||
flushSync() | ||
|
||
document.dispatchEvent(new KeyboardEvent('keyup', { key: 'Delete' })) | ||
expect(spies.add).not.toHaveBeenCalled() | ||
expect(spies.remove).toHaveBeenDispatchedWith('Delete') | ||
expect(spies.submit).not.toHaveBeenCalled() | ||
|
||
cleanup() | ||
}) | ||
|
||
it('should dispatch "remove" event when backspace is pressed', () => { | ||
const cleanup = $effect.root(() => keyboard(node)) | ||
flushSync() | ||
|
||
document.dispatchEvent(new KeyboardEvent('keyup', { key: 'Backspace' })) | ||
expect(spies.add).not.toHaveBeenCalled() | ||
expect(spies.remove).toHaveBeenDispatchedWith('Backspace') | ||
expect(spies.submit).not.toHaveBeenCalled() | ||
|
||
cleanup() | ||
}) | ||
|
||
it('should dispatch "submit" event when backspace is pressed', () => { | ||
const cleanup = $effect.root(() => keyboard(node)) | ||
flushSync() | ||
|
||
document.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' })) | ||
expect(spies.add).not.toHaveBeenCalled() | ||
expect(spies.remove).not.toHaveBeenCalled() | ||
expect(spies.submit).toHaveBeenDispatchedWith('Enter') | ||
|
||
cleanup() | ||
}) | ||
|
||
it('should dispatch "add" event when an alphabet is clicked', () => { | ||
const cleanup = $effect.root(() => keyboard(node)) | ||
flushSync() | ||
|
||
node.querySelector('[data-key="a"]').click() | ||
expect(spies.add).toHaveBeenDispatchedWith('a') | ||
expect(spies.remove).not.toHaveBeenCalled() | ||
expect(spies.submit).not.toHaveBeenCalled() | ||
|
||
cleanup() | ||
}) | ||
|
||
it('should dispatch "remove" event when delete is clicked', () => { | ||
const cleanup = $effect.root(() => keyboard(node)) | ||
flushSync() | ||
|
||
node.querySelector('[data-key="Backspace"]').click() | ||
expect(spies.add).not.toHaveBeenCalled() | ||
expect(spies.remove).toHaveBeenDispatchedWith('Backspace') | ||
expect(spies.submit).not.toHaveBeenCalled() | ||
|
||
cleanup() | ||
}) | ||
|
||
it('should dispatch "submit" event when enter is clicked', () => { | ||
const cleanup = $effect.root(() => keyboard(node)) | ||
flushSync() | ||
|
||
node.querySelector('[data-key="Enter"]').click() | ||
expect(spies.add).not.toHaveBeenCalled() | ||
expect(spies.remove).not.toHaveBeenCalled() | ||
expect(spies.submit).toHaveBeenDispatchedWith('Enter') | ||
|
||
cleanup() | ||
}) | ||
}) |
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,53 @@ | ||
import { describe, it, expect } from 'vitest' | ||
import { getClosestAncestorWithAttribute, getEventForKey } from '../src/utils.js' | ||
|
||
describe('utils', () => { | ||
describe('getClosestAncestorWithAttribute', () => { | ||
it('should return null if element does not have the given attribute and is orphan', () => { | ||
const element = document.createElement('div') | ||
expect(getClosestAncestorWithAttribute(element, 'data-test')).toBe(null) | ||
}) | ||
it('should return the element if it has the given attribute', () => { | ||
const element = document.createElement('div') | ||
element.setAttribute('data-test', 'test') | ||
expect(getClosestAncestorWithAttribute(element, 'data-test')).toBe(element) | ||
}) | ||
it('should return the element if it has the given attribute and is nested', () => { | ||
const parent = document.createElement('div') | ||
const element = document.createElement('div') | ||
element.setAttribute('data-test', 'test') | ||
parent.appendChild(element) | ||
expect(getClosestAncestorWithAttribute(element, 'data-test')).toBe(element) | ||
}) | ||
it('should return the closest ancestor if it has the given attribute', () => { | ||
const element = document.createElement('div') | ||
const parent = document.createElement('div') | ||
parent.setAttribute('data-test', 'test') | ||
parent.appendChild(element) | ||
expect(getClosestAncestorWithAttribute(element, 'data-test')).toBe(parent) | ||
}) | ||
}) | ||
|
||
describe('getEventForKey', () => { | ||
const keyMapping = { | ||
event1: ['a', 'b', 'c'], | ||
event2: /d/, | ||
event3: ['e', 'f'], | ||
event4: /g/ | ||
} | ||
|
||
it('should return the correct event name for a key in an array', () => { | ||
expect(getEventForKey(keyMapping, 'a')).toBe('event1') | ||
expect(getEventForKey(keyMapping, 'f')).toBe('event3') | ||
}) | ||
|
||
it('should return the correct event name for a key matching a regex', () => { | ||
expect(getEventForKey(keyMapping, 'd')).toBe('event2') | ||
expect(getEventForKey(keyMapping, 'g')).toBe('event4') | ||
}) | ||
|
||
it('should return null for a key that does not match any event', () => { | ||
expect(getEventForKey(keyMapping, 'z')).toBeNull() | ||
}) | ||
}) | ||
}) |
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,3 @@ | ||
// skipcq: JS-E1004 - Needed for exposing all types | ||
export * from './types.js' | ||
export { keyboard } from './keyboard.svelte.js' |
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,58 @@ | ||
import { on } from 'svelte/events' | ||
import { getClosestAncestorWithAttribute, getEventForKey } from './utils.js' | ||
|
||
/** | ||
* Default key mappings | ||
* @type {import('./types.js').KeyboardConfig} | ||
*/ | ||
const defaultKeyMappings = { | ||
remove: ['Backspace', 'Delete'], | ||
submit: ['Enter'], | ||
add: /^[a-zA-Z]$/ | ||
} | ||
|
||
/** | ||
* Handle keyboard events | ||
* | ||
* @param {HTMLElement} root | ||
* @param {import('./types.js').KeyboardConfig} options - Custom key mappings | ||
*/ | ||
export function keyboard(root, options = defaultKeyMappings) { | ||
const keyMappings = options || defaultKeyMappings | ||
|
||
/** | ||
* Handle keyboard events | ||
* | ||
* @param {KeyboardEvent} event | ||
*/ | ||
const keyup = (event) => { | ||
const { key } = event | ||
const eventName = getEventForKey(keyMappings, key) | ||
|
||
if (eventName) { | ||
root.dispatchEvent(new CustomEvent(eventName, { detail: key })) | ||
} | ||
} | ||
|
||
const click = (event) => { | ||
const node = getClosestAncestorWithAttribute(event.target, 'data-key') | ||
|
||
if (node) { | ||
const key = node.getAttribute('data-key') | ||
const eventName = getEventForKey(keyMappings, key) | ||
|
||
if (eventName) { | ||
root.dispatchEvent(new CustomEvent(eventName, { detail: key })) | ||
} | ||
} | ||
} | ||
|
||
$effect(() => { | ||
const cleanupKeyupEvent = on(document, 'keyup', keyup) | ||
const cleanupClickEvent = on(root, 'click', click) | ||
return () => { | ||
cleanupKeyupEvent() | ||
cleanupClickEvent() | ||
} | ||
}) | ||
} |
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,10 @@ | ||
/** | ||
* @typedef {Object} EventMapping | ||
* @property {string} event - The event name | ||
* @property {string[]} [keys] - The keys that trigger the event | ||
* @property {RegExp} [pattern] - The pattern that triggers the event | ||
*/ | ||
|
||
/** | ||
* @typedef {Object<string, (string[]|RegExp) >} KeyboardConfig | ||
*/ |
Oops, something went wrong.