Run prettier over everything and fix some imports

This commit is contained in:
Ryan Gossiaux
2021-12-13 18:22:16 -08:00
parent 3bf974a654
commit 82b138f0ae
63 changed files with 3317 additions and 3319 deletions

View File

@@ -1,6 +1,6 @@
<script lang="ts" context="module">
export enum RenderStrategy {
Unmount,
Hidden,
}
export enum RenderStrategy {
Unmount,
Hidden,
}
</script>

View File

@@ -1,84 +1,89 @@
function assertNever(x: never): never {
throw new Error('Unexpected object: ' + x)
throw new Error("Unexpected object: " + x);
}
export enum Focus {
/** Focus the first non-disabled item. */
First,
/** Focus the first non-disabled item. */
First,
/** Focus the previous non-disabled item. */
Previous,
/** Focus the previous non-disabled item. */
Previous,
/** Focus the next non-disabled item. */
Next,
/** Focus the next non-disabled item. */
Next,
/** Focus the last non-disabled item. */
Last,
/** Focus the last non-disabled item. */
Last,
/** Focus a specific item based on the `id` of the item. */
Specific,
/** Focus a specific item based on the `id` of the item. */
Specific,
/** Focus no items at all. */
Nothing,
/** Focus no items at all. */
Nothing,
}
export function calculateActiveIndex<TItem>(
action: { focus: Focus.Specific; id: string } | { focus: Exclude<Focus, Focus.Specific> },
resolvers: {
resolveItems(): TItem[]
resolveActiveIndex(): number | null
resolveId(item: TItem): string
resolveDisabled(item: TItem): boolean
}
action:
| { focus: Focus.Specific; id: string }
| { focus: Exclude<Focus, Focus.Specific> },
resolvers: {
resolveItems(): TItem[];
resolveActiveIndex(): number | null;
resolveId(item: TItem): string;
resolveDisabled(item: TItem): boolean;
}
) {
let items = resolvers.resolveItems()
if (items.length <= 0) return null
let items = resolvers.resolveItems();
if (items.length <= 0) return null;
let currentActiveIndex = resolvers.resolveActiveIndex()
let activeIndex = currentActiveIndex ?? -1
let currentActiveIndex = resolvers.resolveActiveIndex();
let activeIndex = currentActiveIndex ?? -1;
let nextActiveIndex = (() => {
switch (action.focus) {
case Focus.First:
return items.findIndex(item => !resolvers.resolveDisabled(item))
let nextActiveIndex = (() => {
switch (action.focus) {
case Focus.First:
return items.findIndex((item) => !resolvers.resolveDisabled(item));
case Focus.Previous: {
let idx = items
.slice()
.reverse()
.findIndex((item, idx, all) => {
if (activeIndex !== -1 && all.length - idx - 1 >= activeIndex) return false
return !resolvers.resolveDisabled(item)
})
if (idx === -1) return idx
return items.length - 1 - idx
}
case Focus.Previous: {
let idx = items
.slice()
.reverse()
.findIndex((item, idx, all) => {
if (activeIndex !== -1 && all.length - idx - 1 >= activeIndex)
return false;
return !resolvers.resolveDisabled(item);
});
if (idx === -1) return idx;
return items.length - 1 - idx;
}
case Focus.Next:
return items.findIndex((item, idx) => {
if (idx <= activeIndex) return false
return !resolvers.resolveDisabled(item)
})
case Focus.Next:
return items.findIndex((item, idx) => {
if (idx <= activeIndex) return false;
return !resolvers.resolveDisabled(item);
});
case Focus.Last: {
let idx = items
.slice()
.reverse()
.findIndex(item => !resolvers.resolveDisabled(item))
if (idx === -1) return idx
return items.length - 1 - idx
}
case Focus.Last: {
let idx = items
.slice()
.reverse()
.findIndex((item) => !resolvers.resolveDisabled(item));
if (idx === -1) return idx;
return items.length - 1 - idx;
}
case Focus.Specific:
return items.findIndex(item => resolvers.resolveId(item) === action.id)
case Focus.Specific:
return items.findIndex(
(item) => resolvers.resolveId(item) === action.id
);
case Focus.Nothing:
return null
case Focus.Nothing:
return null;
default:
assertNever(action)
}
})()
default:
assertNever(action);
}
})();
return nextActiveIndex === -1 ? currentActiveIndex : nextActiveIndex
return nextActiveIndex === -1 ? currentActiveIndex : nextActiveIndex;
}

View File

@@ -1,33 +1,33 @@
export function disposables() {
let disposables: Function[] = []
let disposables: Function[] = [];
let api = {
requestAnimationFrame(...args: Parameters<typeof requestAnimationFrame>) {
let raf = requestAnimationFrame(...args)
api.add(() => cancelAnimationFrame(raf))
},
let api = {
requestAnimationFrame(...args: Parameters<typeof requestAnimationFrame>) {
let raf = requestAnimationFrame(...args);
api.add(() => cancelAnimationFrame(raf));
},
nextFrame(...args: Parameters<typeof requestAnimationFrame>) {
api.requestAnimationFrame(() => {
api.requestAnimationFrame(...args)
})
},
nextFrame(...args: Parameters<typeof requestAnimationFrame>) {
api.requestAnimationFrame(() => {
api.requestAnimationFrame(...args);
});
},
setTimeout(...args: Parameters<typeof setTimeout>) {
let timer = setTimeout(...args)
api.add(() => clearTimeout(timer))
},
setTimeout(...args: Parameters<typeof setTimeout>) {
let timer = setTimeout(...args);
api.add(() => clearTimeout(timer));
},
add(cb: () => void) {
disposables.push(cb)
},
add(cb: () => void) {
disposables.push(cb);
},
dispose() {
for (let dispose of disposables.splice(0)) {
dispose()
}
},
}
dispose() {
for (let dispose of disposables.splice(0)) {
dispose();
}
},
};
return api
return api;
}

View File

@@ -1,155 +1,165 @@
import { match } from './match'
import { match } from "./match";
// Credit:
// - https://stackoverflow.com/a/30753870
let focusableSelector = [
'[contentEditable=true]',
'[tabindex]',
'a[href]',
'area[href]',
'button:not([disabled])',
'iframe',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
"[contentEditable=true]",
"[tabindex]",
"a[href]",
"area[href]",
"button:not([disabled])",
"iframe",
"input:not([disabled])",
"select:not([disabled])",
"textarea:not([disabled])",
]
.map(
process.env.NODE_ENV === 'test'
? // TODO: Remove this once JSDOM fixes the issue where an element that is
// "hidden" can be the document.activeElement, because this is not possible
// in real browsers.
selector => `${selector}:not([tabindex='-1']):not([style*='display: none'])`
: selector => `${selector}:not([tabindex='-1'])`
)
.join(',')
.map(
process.env.NODE_ENV === "test"
? // TODO: Remove this once JSDOM fixes the issue where an element that is
// "hidden" can be the document.activeElement, because this is not possible
// in real browsers.
(selector) =>
`${selector}:not([tabindex='-1']):not([style*='display: none'])`
: (selector) => `${selector}:not([tabindex='-1'])`
)
.join(",");
export enum Focus {
/** Focus the first non-disabled element */
First = 1 << 0,
/** Focus the first non-disabled element */
First = 1 << 0,
/** Focus the previous non-disabled element */
Previous = 1 << 1,
/** Focus the previous non-disabled element */
Previous = 1 << 1,
/** Focus the next non-disabled element */
Next = 1 << 2,
/** Focus the next non-disabled element */
Next = 1 << 2,
/** Focus the last non-disabled element */
Last = 1 << 3,
/** Focus the last non-disabled element */
Last = 1 << 3,
/** Wrap tab around */
WrapAround = 1 << 4,
/** Wrap tab around */
WrapAround = 1 << 4,
/** Prevent scrolling the focusable elements into view */
NoScroll = 1 << 5,
/** Prevent scrolling the focusable elements into view */
NoScroll = 1 << 5,
}
export enum FocusResult {
Error,
Overflow,
Success,
Underflow,
Error,
Overflow,
Success,
Underflow,
}
enum Direction {
Previous = -1,
Next = 1,
Previous = -1,
Next = 1,
}
export function getFocusableElements(container: HTMLElement | null = document.body) {
if (container == null) return []
return Array.from(container.querySelectorAll<HTMLElement>(focusableSelector))
export function getFocusableElements(
container: HTMLElement | null = document.body
) {
if (container == null) return [];
return Array.from(container.querySelectorAll<HTMLElement>(focusableSelector));
}
export enum FocusableMode {
/** The element itself must be focusable. */
Strict,
/** The element itself must be focusable. */
Strict,
/** The element should be inside of a focusable element. */
Loose,
/** The element should be inside of a focusable element. */
Loose,
}
export function isFocusableElement(
element: HTMLElement,
mode: FocusableMode = FocusableMode.Strict
element: HTMLElement,
mode: FocusableMode = FocusableMode.Strict
) {
if (element === document.body) return false
if (element === document.body) return false;
return match(mode, {
[FocusableMode.Strict]() {
return element.matches(focusableSelector)
},
[FocusableMode.Loose]() {
let next: HTMLElement | null = element
return match(mode, {
[FocusableMode.Strict]() {
return element.matches(focusableSelector);
},
[FocusableMode.Loose]() {
let next: HTMLElement | null = element;
while (next !== null) {
if (next.matches(focusableSelector)) return true
next = next.parentElement
}
while (next !== null) {
if (next.matches(focusableSelector)) return true;
next = next.parentElement;
}
return false
},
})
return false;
},
});
}
export function focusElement(element: HTMLElement | null) {
element?.focus({ preventScroll: true })
element?.focus({ preventScroll: true });
}
export function focusIn(container: HTMLElement | HTMLElement[], focus: Focus) {
let elements = Array.isArray(container) ? container : getFocusableElements(container)
let active = document.activeElement as HTMLElement
let elements = Array.isArray(container)
? container
: getFocusableElements(container);
let active = document.activeElement as HTMLElement;
let direction = (() => {
if (focus & (Focus.First | Focus.Next)) return Direction.Next
if (focus & (Focus.Previous | Focus.Last)) return Direction.Previous
let direction = (() => {
if (focus & (Focus.First | Focus.Next)) return Direction.Next;
if (focus & (Focus.Previous | Focus.Last)) return Direction.Previous;
throw new Error('Missing Focus.First, Focus.Previous, Focus.Next or Focus.Last')
})()
throw new Error(
"Missing Focus.First, Focus.Previous, Focus.Next or Focus.Last"
);
})();
let startIndex = (() => {
if (focus & Focus.First) return 0
if (focus & Focus.Previous) return Math.max(0, elements.indexOf(active)) - 1
if (focus & Focus.Next) return Math.max(0, elements.indexOf(active)) + 1
if (focus & Focus.Last) return elements.length - 1
let startIndex = (() => {
if (focus & Focus.First) return 0;
if (focus & Focus.Previous)
return Math.max(0, elements.indexOf(active)) - 1;
if (focus & Focus.Next) return Math.max(0, elements.indexOf(active)) + 1;
if (focus & Focus.Last) return elements.length - 1;
throw new Error('Missing Focus.First, Focus.Previous, Focus.Next or Focus.Last')
})()
throw new Error(
"Missing Focus.First, Focus.Previous, Focus.Next or Focus.Last"
);
})();
let focusOptions = focus & Focus.NoScroll ? { preventScroll: true } : {}
let focusOptions = focus & Focus.NoScroll ? { preventScroll: true } : {};
let offset = 0
let total = elements.length
let next = undefined
do {
// Guard against infinite loops
if (offset >= total || offset + total <= 0) return FocusResult.Error
let offset = 0;
let total = elements.length;
let next = undefined;
do {
// Guard against infinite loops
if (offset >= total || offset + total <= 0) return FocusResult.Error;
let nextIdx = startIndex + offset
let nextIdx = startIndex + offset;
if (focus & Focus.WrapAround) {
nextIdx = (nextIdx + total) % total
} else {
if (nextIdx < 0) return FocusResult.Underflow
if (nextIdx >= total) return FocusResult.Overflow
}
if (focus & Focus.WrapAround) {
nextIdx = (nextIdx + total) % total;
} else {
if (nextIdx < 0) return FocusResult.Underflow;
if (nextIdx >= total) return FocusResult.Overflow;
}
next = elements[nextIdx]
next = elements[nextIdx];
// Try the focus the next element, might not work if it is "hidden" to the user.
next?.focus(focusOptions)
// Try the focus the next element, might not work if it is "hidden" to the user.
next?.focus(focusOptions);
// Try the next one in line
offset += direction
} while (next !== document.activeElement)
// Try the next one in line
offset += direction;
} while (next !== document.activeElement);
// This is a little weird, but let me try and explain: There are a few scenario's
// in chrome for example where a focused `<a>` tag does not get the default focus
// styles and sometimes they do. This highly depends on whether you started by
// clicking or by using your keyboard. When you programmatically add focus `anchor.focus()`
// then the active element (document.activeElement) is this anchor, which is expected.
// However in that case the default focus styles are not applied *unless* you
// also add this tabindex.
if (!next.hasAttribute('tabindex')) next.setAttribute('tabindex', '0')
// This is a little weird, but let me try and explain: There are a few scenario's
// in chrome for example where a focused `<a>` tag does not get the default focus
// styles and sometimes they do. This highly depends on whether you started by
// clicking or by using your keyboard. When you programmatically add focus `anchor.focus()`
// then the active element (document.activeElement) is this anchor, which is expected.
// However in that case the default focus styles are not applied *unless* you
// also add this tabindex.
if (!next.hasAttribute("tabindex")) next.setAttribute("tabindex", "0");
return FocusResult.Success
return FocusResult.Success;
}

View File

@@ -1,21 +1,21 @@
// TODO: This must already exist somewhere, right? 🤔
// Ref: https://www.w3.org/TR/uievents-key/#named-key-attribute-values
export enum Keys {
Space = ' ',
Enter = 'Enter',
Escape = 'Escape',
Backspace = 'Backspace',
Space = " ",
Enter = "Enter",
Escape = "Escape",
Backspace = "Backspace",
ArrowLeft = 'ArrowLeft',
ArrowUp = 'ArrowUp',
ArrowRight = 'ArrowRight',
ArrowDown = 'ArrowDown',
ArrowLeft = "ArrowLeft",
ArrowUp = "ArrowUp",
ArrowRight = "ArrowRight",
ArrowDown = "ArrowDown",
Home = 'Home',
End = 'End',
Home = "Home",
End = "End",
PageUp = 'PageUp',
PageDown = 'PageDown',
PageUp = "PageUp",
PageDown = "PageDown",
Tab = 'Tab',
Tab = "Tab",
}

View File

@@ -1,20 +1,25 @@
export function match<TValue extends string | number = string, TReturnValue = unknown>(
value: TValue,
lookup: Record<TValue, TReturnValue | ((...args: any[]) => TReturnValue)>,
...args: any[]
export function match<
TValue extends string | number = string,
TReturnValue = unknown
>(
value: TValue,
lookup: Record<TValue, TReturnValue | ((...args: any[]) => TReturnValue)>,
...args: any[]
): TReturnValue {
if (value in lookup) {
let returnValue = lookup[value]
return typeof returnValue === 'function' ? returnValue(...args) : returnValue
}
if (value in lookup) {
let returnValue = lookup[value];
return typeof returnValue === "function"
? returnValue(...args)
: returnValue;
}
let error = new Error(
`Tried to handle "${value}" but there is no handler defined. Only defined handlers are: ${Object.keys(
lookup
)
.map(key => `"${key}"`)
.join(', ')}.`
let error = new Error(
`Tried to handle "${value}" but there is no handler defined. Only defined handlers are: ${Object.keys(
lookup
)
if (Error.captureStackTrace) Error.captureStackTrace(error, match)
throw error
.map((key) => `"${key}"`)
.join(", ")}.`
);
if (Error.captureStackTrace) Error.captureStackTrace(error, match);
throw error;
}

View File

@@ -1,9 +1,9 @@
export function once<T>(cb: (...args: T[]) => void) {
let state = { called: false }
let state = { called: false };
return (...args: T[]) => {
if (state.called) return
state.called = true
return cb(...args)
}
return (...args: T[]) => {
if (state.called) return;
state.called = true;
return cb(...args);
};
}

View File

@@ -1,95 +1,97 @@
import { once } from './once'
import { disposables } from './disposables'
import { once } from "./once";
import { disposables } from "./disposables";
function addClasses(node: HTMLElement, ...classes: string[]) {
node && classes.length > 0 && node.classList.add(...classes)
node && classes.length > 0 && node.classList.add(...classes);
}
function removeClasses(node: HTMLElement, ...classes: string[]) {
node && classes.length > 0 && node.classList.remove(...classes)
node && classes.length > 0 && node.classList.remove(...classes);
}
export enum Reason {
Finished = 'finished',
Cancelled = 'cancelled',
Finished = "finished",
Cancelled = "cancelled",
}
function waitForTransition(node: HTMLElement, done: (reason: Reason) => void) {
let d = disposables()
let d = disposables();
if (!node) return d.dispose
if (!node) return d.dispose;
// Safari returns a comma separated list of values, so let's sort them and take the highest value.
let { transitionDuration, transitionDelay } = getComputedStyle(node)
// Safari returns a comma separated list of values, so let's sort them and take the highest value.
let { transitionDuration, transitionDelay } = getComputedStyle(node);
let [durationMs, delaysMs] = [transitionDuration, transitionDelay].map(value => {
let [resolvedValue = 0] = value
.split(',')
// Remove falsy we can't work with
.filter(Boolean)
// Values are returned as `0.3s` or `75ms`
.map(v => (v.includes('ms') ? parseFloat(v) : parseFloat(v) * 1000))
.sort((a, z) => z - a)
let [durationMs, delaysMs] = [transitionDuration, transitionDelay].map(
(value) => {
let [resolvedValue = 0] = value
.split(",")
// Remove falsy we can't work with
.filter(Boolean)
// Values are returned as `0.3s` or `75ms`
.map((v) => (v.includes("ms") ? parseFloat(v) : parseFloat(v) * 1000))
.sort((a, z) => z - a);
return resolvedValue
})
// Waiting for the transition to end. We could use the `transitionend` event, however when no
// actual transition/duration is defined then the `transitionend` event is not fired.
//
// TODO: Downside is, when you slow down transitions via devtools this timeout is still using the
// full 100% speed instead of the 25% or 10%.
if (durationMs !== 0) {
d.setTimeout(() => {
done(Reason.Finished)
}, durationMs + delaysMs)
} else {
// No transition is happening, so we should cleanup already. Otherwise we have to wait until we
// get disposed.
done(Reason.Finished)
return resolvedValue;
}
);
// If we get disposed before the timeout runs we should cleanup anyway
d.add(() => done(Reason.Cancelled))
// Waiting for the transition to end. We could use the `transitionend` event, however when no
// actual transition/duration is defined then the `transitionend` event is not fired.
//
// TODO: Downside is, when you slow down transitions via devtools this timeout is still using the
// full 100% speed instead of the 25% or 10%.
if (durationMs !== 0) {
d.setTimeout(() => {
done(Reason.Finished);
}, durationMs + delaysMs);
} else {
// No transition is happening, so we should cleanup already. Otherwise we have to wait until we
// get disposed.
done(Reason.Finished);
}
return d.dispose
// If we get disposed before the timeout runs we should cleanup anyway
d.add(() => done(Reason.Cancelled));
return d.dispose;
}
export function transition(
node: HTMLElement,
base: string[],
from: string[],
to: string[],
entered: string[],
done?: (reason: Reason) => void
node: HTMLElement,
base: string[],
from: string[],
to: string[],
entered: string[],
done?: (reason: Reason) => void
) {
let d = disposables()
let _done = done !== undefined ? once(done) : () => { }
let d = disposables();
let _done = done !== undefined ? once(done) : () => {};
removeClasses(node, ...entered)
addClasses(node, ...base, ...from)
removeClasses(node, ...entered);
addClasses(node, ...base, ...from);
d.nextFrame(() => {
removeClasses(node, ...from)
addClasses(node, ...to)
d.nextFrame(() => {
removeClasses(node, ...from);
addClasses(node, ...to);
d.add(
waitForTransition(node, reason => {
removeClasses(node, ...to, ...base)
addClasses(node, ...entered)
return _done(reason)
})
)
})
d.add(
waitForTransition(node, (reason) => {
removeClasses(node, ...to, ...base);
addClasses(node, ...entered);
return _done(reason);
})
);
});
// Once we get disposed, we should ensure that we cleanup after ourselves. In case of an unmount,
// the node itself will be nullified and will be a no-op. In case of a full transition the classes
// are already removed which is also a no-op. However if you go from enter -> leave mid-transition
// then we have some leftovers that should be cleaned.
d.add(() => removeClasses(node, ...base, ...from, ...to, ...entered))
// Once we get disposed, we should ensure that we cleanup after ourselves. In case of an unmount,
// the node itself will be nullified and will be a no-op. In case of a full transition the classes
// are already removed which is also a no-op. However if you go from enter -> leave mid-transition
// then we have some leftovers that should be cleaned.
d.add(() => removeClasses(node, ...base, ...from, ...to, ...entered));
// When we get disposed early, than we should also call the done method but switch the reason.
d.add(() => _done(Reason.Cancelled))
// When we get disposed early, than we should also call the done method but switch the reason.
d.add(() => _done(Reason.Cancelled));
return d.dispose
return d.dispose;
}

View File

@@ -1,28 +1,35 @@
type AcceptNode = (
node: HTMLElement
node: HTMLElement
) =>
| typeof NodeFilter.FILTER_ACCEPT
| typeof NodeFilter.FILTER_SKIP
| typeof NodeFilter.FILTER_REJECT
| typeof NodeFilter.FILTER_ACCEPT
| typeof NodeFilter.FILTER_SKIP
| typeof NodeFilter.FILTER_REJECT;
export function treeWalker({
container,
accept,
walk,
enabled,
container,
accept,
walk,
enabled,
}: {
container: HTMLElement | null
accept: AcceptNode
walk(node: HTMLElement): void
enabled?: boolean
container: HTMLElement | null;
accept: AcceptNode;
walk(node: HTMLElement): void;
enabled?: boolean;
}) {
let root = container
if (!root) return
if (enabled !== undefined && !enabled) return
let root = container;
if (!root) return;
if (enabled !== undefined && !enabled) return;
let acceptNode = Object.assign((node: HTMLElement) => accept(node), { acceptNode: accept })
// @ts-ignore-error Typescript bug thinks this can only have 3 args
let walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, acceptNode, false)
let acceptNode = Object.assign((node: HTMLElement) => accept(node), {
acceptNode: accept,
});
// @ts-ignore-error Typescript bug thinks this can only have 3 args
let walker = document.createTreeWalker(
root,
NodeFilter.SHOW_ELEMENT,
acceptNode,
false
);
while (walker.nextNode()) walk(walker.currentNode as HTMLElement)
while (walker.nextNode()) walk(walker.currentNode as HTMLElement);
}