Run prettier over everything and fix some imports
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script lang="ts" context="module">
|
||||
export enum RenderStrategy {
|
||||
Unmount,
|
||||
Hidden,
|
||||
}
|
||||
export enum RenderStrategy {
|
||||
Unmount,
|
||||
Hidden,
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user