@octanejs/select 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +43 -0
- package/UPSTREAM.md +66 -0
- package/package.json +89 -0
- package/src/accessibility-types.ts +70 -0
- package/src/animated/component-element.ts +5 -0
- package/src/animated/index.ts +32 -0
- package/src/animated/input.tsrx +24 -0
- package/src/animated/multi-value.tsrx +23 -0
- package/src/animated/placeholder.tsrx +19 -0
- package/src/animated/single-value.tsrx +18 -0
- package/src/animated/transitions.tsrx +90 -0
- package/src/animated/value-container.tsrx +96 -0
- package/src/animated-registry.ts +10 -0
- package/src/async-creatable.tsrx +30 -0
- package/src/async.tsrx +19 -0
- package/src/base.ts +7 -0
- package/src/builtins.ts +13 -0
- package/src/components.tsrx +875 -0
- package/src/creatable.tsrx +26 -0
- package/src/default-styles.ts +445 -0
- package/src/diacritics.ts +245 -0
- package/src/filters.ts +49 -0
- package/src/index.ts +66 -0
- package/src/menu-placement.ts +143 -0
- package/src/scroll-manager.ts +112 -0
- package/src/select.tsrx +1621 -0
- package/src/state-managed-select.tsrx +19 -0
- package/src/style-adapter.d.ts +25 -0
- package/src/style-adapter.mjs +53 -0
- package/src/style-context.tsrx +37 -0
- package/src/styles.ts +53 -0
- package/src/theme.ts +35 -0
- package/src/types.ts +147 -0
- package/src/use-async.tsrx +146 -0
- package/src/use-creatable.tsrx +151 -0
- package/src/use-state-manager.tsrx +90 -0
- package/src/utils.ts +71 -0
package/src/filters.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import memoizeOne from 'memoize-one';
|
|
2
|
+
|
|
3
|
+
import { stripDiacritics } from './diacritics';
|
|
4
|
+
|
|
5
|
+
export interface FilterOptionOption<Option> {
|
|
6
|
+
readonly label: string;
|
|
7
|
+
readonly value: string;
|
|
8
|
+
readonly data: Option;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface Config<Option> {
|
|
12
|
+
readonly ignoreCase?: boolean;
|
|
13
|
+
readonly ignoreAccents?: boolean;
|
|
14
|
+
readonly stringify?: (option: FilterOptionOption<Option>) => string;
|
|
15
|
+
readonly trim?: boolean;
|
|
16
|
+
readonly matchFrom?: 'any' | 'start';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const memoizedStripDiacriticsForInput = memoizeOne(stripDiacritics);
|
|
20
|
+
const trimString = (value: string) => value.replace(/^\s+|\s+$/g, '');
|
|
21
|
+
const defaultStringify = <Option>(option: FilterOptionOption<Option>) =>
|
|
22
|
+
`${option.label} ${option.value}`;
|
|
23
|
+
|
|
24
|
+
export const createFilter =
|
|
25
|
+
<Option>(config?: Config<Option>) =>
|
|
26
|
+
(option: FilterOptionOption<Option>, rawInput: string): boolean => {
|
|
27
|
+
if ((option.data as { __isNew__?: unknown }).__isNew__) return true;
|
|
28
|
+
const { ignoreCase, ignoreAccents, stringify, trim, matchFrom } = {
|
|
29
|
+
ignoreCase: true,
|
|
30
|
+
ignoreAccents: true,
|
|
31
|
+
stringify: defaultStringify,
|
|
32
|
+
trim: true,
|
|
33
|
+
matchFrom: 'any' as const,
|
|
34
|
+
...config,
|
|
35
|
+
};
|
|
36
|
+
let input = trim ? trimString(rawInput) : rawInput;
|
|
37
|
+
let candidate = trim ? trimString(stringify(option)) : stringify(option);
|
|
38
|
+
if (ignoreCase) {
|
|
39
|
+
input = input.toLowerCase();
|
|
40
|
+
candidate = candidate.toLowerCase();
|
|
41
|
+
}
|
|
42
|
+
if (ignoreAccents) {
|
|
43
|
+
input = memoizedStripDiacriticsForInput(input);
|
|
44
|
+
candidate = stripDiacritics(candidate);
|
|
45
|
+
}
|
|
46
|
+
return matchFrom === 'start'
|
|
47
|
+
? candidate.slice(0, input.length) === input
|
|
48
|
+
: candidate.indexOf(input) > -1;
|
|
49
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export { mergeStyles } from './styles';
|
|
2
|
+
export type { ClassNamesConfig, StylesConfig } from './styles';
|
|
3
|
+
export { defaultTheme } from './theme';
|
|
4
|
+
export type { ThemeConfig } from './theme';
|
|
5
|
+
export type {
|
|
6
|
+
Colors,
|
|
7
|
+
CSSObjectWithLabel,
|
|
8
|
+
GroupBase,
|
|
9
|
+
InputAction,
|
|
10
|
+
InputActionMeta,
|
|
11
|
+
MultiValue,
|
|
12
|
+
Options,
|
|
13
|
+
PropsValue,
|
|
14
|
+
SingleValue,
|
|
15
|
+
Theme,
|
|
16
|
+
ThemeSpacing,
|
|
17
|
+
} from './types';
|
|
18
|
+
export { createFilter } from './filters';
|
|
19
|
+
export type { FilterOptionOption } from './filters';
|
|
20
|
+
export * from './types';
|
|
21
|
+
export type {
|
|
22
|
+
AriaGuidance,
|
|
23
|
+
AriaGuidanceProps,
|
|
24
|
+
AriaLiveMessages,
|
|
25
|
+
AriaOnChange,
|
|
26
|
+
AriaOnChangeProps,
|
|
27
|
+
AriaOnFilter,
|
|
28
|
+
AriaOnFilterProps,
|
|
29
|
+
AriaOnFocus,
|
|
30
|
+
AriaOnFocusProps,
|
|
31
|
+
GuidanceContext,
|
|
32
|
+
OptionContext,
|
|
33
|
+
} from './accessibility-types';
|
|
34
|
+
export { StateManagedSelect as default } from './state-managed-select.tsrx';
|
|
35
|
+
export { NonceProvider } from './style-context.tsrx';
|
|
36
|
+
export { components } from './components.tsrx';
|
|
37
|
+
export { useStateManager } from './use-state-manager.tsrx';
|
|
38
|
+
export type {
|
|
39
|
+
ContainerProps,
|
|
40
|
+
ControlProps,
|
|
41
|
+
GroupHeadingProps,
|
|
42
|
+
GroupProps,
|
|
43
|
+
IndicatorProps as ClearIndicatorProps,
|
|
44
|
+
IndicatorProps as DropdownIndicatorProps,
|
|
45
|
+
IndicatorSeparatorProps,
|
|
46
|
+
IndicatorsContainerProps,
|
|
47
|
+
InputProps,
|
|
48
|
+
LoadingIndicatorProps,
|
|
49
|
+
MenuListProps,
|
|
50
|
+
MenuProps,
|
|
51
|
+
MultiValueGenericProps,
|
|
52
|
+
MultiValueProps,
|
|
53
|
+
MultiValueRemoveProps,
|
|
54
|
+
NoticeProps,
|
|
55
|
+
OptionProps,
|
|
56
|
+
PlaceholderProps,
|
|
57
|
+
SelectComponentsConfig,
|
|
58
|
+
SingleValueProps,
|
|
59
|
+
ValueContainerProps,
|
|
60
|
+
} from './components.tsrx';
|
|
61
|
+
export type {
|
|
62
|
+
FormatOptionLabelContext,
|
|
63
|
+
FormatOptionLabelMeta,
|
|
64
|
+
SelectInstance,
|
|
65
|
+
} from './select.tsrx';
|
|
66
|
+
export type { Props } from './state-managed-select.tsrx';
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { CoercedMenuPlacement, MenuPlacement } from './types';
|
|
2
|
+
|
|
3
|
+
type ScrollTarget = HTMLElement | Window;
|
|
4
|
+
|
|
5
|
+
export interface PlacementArgs {
|
|
6
|
+
maxHeight: number;
|
|
7
|
+
menuEl: HTMLElement | null;
|
|
8
|
+
minHeight: number;
|
|
9
|
+
placement: MenuPlacement;
|
|
10
|
+
shouldScroll: boolean;
|
|
11
|
+
isFixedPosition: boolean;
|
|
12
|
+
controlHeight: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CalculatedMenuPlacementAndHeight {
|
|
16
|
+
placement: CoercedMenuPlacement;
|
|
17
|
+
maxHeight: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isDocumentElement(element: ScrollTarget) {
|
|
21
|
+
return element === document.documentElement || element === document.body || element === window;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizedHeight(element: ScrollTarget) {
|
|
25
|
+
return isDocumentElement(element) ? window.innerHeight : (element as HTMLElement).clientHeight;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getScrollTop(element: ScrollTarget) {
|
|
29
|
+
return isDocumentElement(element) ? window.pageYOffset : (element as HTMLElement).scrollTop;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function scrollTo(element: ScrollTarget, top: number) {
|
|
33
|
+
if (isDocumentElement(element)) window.scrollTo(0, top);
|
|
34
|
+
else (element as HTMLElement).scrollTop = top;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getScrollParent(element: HTMLElement | null): ScrollTarget {
|
|
38
|
+
if (!element) return typeof document === 'undefined' ? ({} as Window) : document.documentElement;
|
|
39
|
+
let style = getComputedStyle(element);
|
|
40
|
+
const excludeStaticParent = style.position === 'absolute';
|
|
41
|
+
if (style.position === 'fixed') return document.documentElement;
|
|
42
|
+
for (let parent = element.parentElement; parent; parent = parent.parentElement) {
|
|
43
|
+
style = getComputedStyle(parent);
|
|
44
|
+
if (excludeStaticParent && style.position === 'static') continue;
|
|
45
|
+
if (/(auto|scroll)/.test(`${style.overflow}${style.overflowY}${style.overflowX}`)) {
|
|
46
|
+
return parent;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return document.documentElement;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function animatedScrollTo(element: ScrollTarget, to: number, duration = 200) {
|
|
53
|
+
const start = getScrollTop(element);
|
|
54
|
+
const change = to - start;
|
|
55
|
+
const increment = 10;
|
|
56
|
+
let currentTime = 0;
|
|
57
|
+
const animate = () => {
|
|
58
|
+
currentTime += increment;
|
|
59
|
+
const elapsed = currentTime / duration - 1;
|
|
60
|
+
scrollTo(element, change * (elapsed * elapsed * elapsed + 1) + start);
|
|
61
|
+
if (currentTime < duration) window.requestAnimationFrame(animate);
|
|
62
|
+
};
|
|
63
|
+
animate();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getMenuPlacement({
|
|
67
|
+
maxHeight: preferredMaxHeight,
|
|
68
|
+
menuEl,
|
|
69
|
+
minHeight,
|
|
70
|
+
placement: preferredPlacement,
|
|
71
|
+
shouldScroll,
|
|
72
|
+
isFixedPosition,
|
|
73
|
+
controlHeight,
|
|
74
|
+
}: PlacementArgs): CalculatedMenuPlacementAndHeight {
|
|
75
|
+
const defaultState = { placement: 'bottom' as const, maxHeight: preferredMaxHeight };
|
|
76
|
+
if (!menuEl || !menuEl.offsetParent) return defaultState;
|
|
77
|
+
|
|
78
|
+
const scrollParent = getScrollParent(menuEl);
|
|
79
|
+
const scrollHeight = (scrollParent as HTMLElement).getBoundingClientRect().height;
|
|
80
|
+
const { bottom: menuBottom, height: menuHeight, top: menuTop } = menuEl.getBoundingClientRect();
|
|
81
|
+
const containerTop = (menuEl.offsetParent as HTMLElement).getBoundingClientRect().top;
|
|
82
|
+
const viewHeight = isFixedPosition ? window.innerHeight : normalizedHeight(scrollParent);
|
|
83
|
+
const scrollTop = getScrollTop(scrollParent);
|
|
84
|
+
const marginBottom = Number.parseInt(getComputedStyle(menuEl).marginBottom, 10);
|
|
85
|
+
const marginTop = Number.parseInt(getComputedStyle(menuEl).marginTop, 10);
|
|
86
|
+
const viewSpaceAbove = containerTop - marginTop;
|
|
87
|
+
const viewSpaceBelow = viewHeight - menuTop;
|
|
88
|
+
const scrollSpaceAbove = viewSpaceAbove + scrollTop;
|
|
89
|
+
const scrollSpaceBelow = scrollHeight - scrollTop - menuTop;
|
|
90
|
+
const scrollDown = menuBottom - viewHeight + scrollTop + marginBottom;
|
|
91
|
+
const scrollUp = scrollTop + menuTop - marginTop;
|
|
92
|
+
|
|
93
|
+
if (preferredPlacement === 'auto' || preferredPlacement === 'bottom') {
|
|
94
|
+
if (viewSpaceBelow >= menuHeight) return defaultState;
|
|
95
|
+
if (scrollSpaceBelow >= menuHeight && !isFixedPosition) {
|
|
96
|
+
if (shouldScroll) animatedScrollTo(scrollParent, scrollDown, 160);
|
|
97
|
+
return defaultState;
|
|
98
|
+
}
|
|
99
|
+
if (
|
|
100
|
+
(!isFixedPosition && scrollSpaceBelow >= minHeight) ||
|
|
101
|
+
(isFixedPosition && viewSpaceBelow >= minHeight)
|
|
102
|
+
) {
|
|
103
|
+
if (shouldScroll) animatedScrollTo(scrollParent, scrollDown, 160);
|
|
104
|
+
return {
|
|
105
|
+
placement: 'bottom',
|
|
106
|
+
maxHeight: (isFixedPosition ? viewSpaceBelow : scrollSpaceBelow) - marginBottom,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (preferredPlacement === 'auto' || isFixedPosition) {
|
|
110
|
+
const spaceAbove = isFixedPosition ? viewSpaceAbove : scrollSpaceAbove;
|
|
111
|
+
return {
|
|
112
|
+
placement: 'top',
|
|
113
|
+
maxHeight:
|
|
114
|
+
spaceAbove >= minHeight
|
|
115
|
+
? Math.min(spaceAbove - marginBottom - controlHeight, preferredMaxHeight)
|
|
116
|
+
: preferredMaxHeight,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (shouldScroll) scrollTo(scrollParent, scrollDown);
|
|
120
|
+
return defaultState;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (preferredPlacement === 'top') {
|
|
124
|
+
if (viewSpaceAbove >= menuHeight) return { placement: 'top', maxHeight: preferredMaxHeight };
|
|
125
|
+
if (scrollSpaceAbove >= menuHeight && !isFixedPosition) {
|
|
126
|
+
if (shouldScroll) animatedScrollTo(scrollParent, scrollUp, 160);
|
|
127
|
+
return { placement: 'top', maxHeight: preferredMaxHeight };
|
|
128
|
+
}
|
|
129
|
+
if (
|
|
130
|
+
(!isFixedPosition && scrollSpaceAbove >= minHeight) ||
|
|
131
|
+
(isFixedPosition && viewSpaceAbove >= minHeight)
|
|
132
|
+
) {
|
|
133
|
+
if (shouldScroll) animatedScrollTo(scrollParent, scrollUp, 160);
|
|
134
|
+
return {
|
|
135
|
+
placement: 'top',
|
|
136
|
+
maxHeight: (isFixedPosition ? viewSpaceAbove : scrollSpaceAbove) - marginTop,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return defaultState;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
throw new Error(`Invalid placement provided "${preferredPlacement}".`);
|
|
143
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'octane';
|
|
2
|
+
|
|
3
|
+
interface ScrollManagerOptions {
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
captureEnabled: boolean;
|
|
6
|
+
lockEnabled: boolean;
|
|
7
|
+
target: { current: HTMLElement | null };
|
|
8
|
+
onBottomArrive?: (event: WheelEvent | TouchEvent) => void;
|
|
9
|
+
onTopArrive?: (event: WheelEvent | TouchEvent) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let activeScrollLocks = 0;
|
|
13
|
+
let originalBodyStyle: {
|
|
14
|
+
boxSizing: string;
|
|
15
|
+
height: string;
|
|
16
|
+
overflow: string;
|
|
17
|
+
paddingRight: string;
|
|
18
|
+
position: string;
|
|
19
|
+
} | null = null;
|
|
20
|
+
|
|
21
|
+
function cancelScroll(event: Event) {
|
|
22
|
+
if (event.cancelable) event.preventDefault();
|
|
23
|
+
event.stopPropagation();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function useScrollManager(options: ScrollManagerOptions) {
|
|
27
|
+
const atBottom = useRef(false);
|
|
28
|
+
const atTop = useRef(false);
|
|
29
|
+
const touchStart = useRef(0);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const element = options.target.current;
|
|
33
|
+
if (!options.enabled || !options.captureEnabled || !element) return;
|
|
34
|
+
|
|
35
|
+
const handleDelta = (event: WheelEvent | TouchEvent, delta: number) => {
|
|
36
|
+
const { scrollTop, scrollHeight, clientHeight } = element;
|
|
37
|
+
const available = scrollHeight - clientHeight - scrollTop;
|
|
38
|
+
let nextDelta = delta;
|
|
39
|
+
if (delta > available && available > 0) {
|
|
40
|
+
element.scrollTop = scrollHeight;
|
|
41
|
+
nextDelta = 0;
|
|
42
|
+
} else if (delta > 0 && available <= 0) {
|
|
43
|
+
if (!atBottom.current) options.onBottomArrive?.(event);
|
|
44
|
+
atBottom.current = true;
|
|
45
|
+
nextDelta = 0;
|
|
46
|
+
} else if (delta < -scrollTop && scrollTop > 0) {
|
|
47
|
+
element.scrollTop = 0;
|
|
48
|
+
nextDelta = 0;
|
|
49
|
+
} else if (delta < 0 && scrollTop <= 0) {
|
|
50
|
+
if (!atTop.current) options.onTopArrive?.(event);
|
|
51
|
+
atTop.current = true;
|
|
52
|
+
nextDelta = 0;
|
|
53
|
+
}
|
|
54
|
+
if (nextDelta !== 0) {
|
|
55
|
+
atTop.current = false;
|
|
56
|
+
atBottom.current = false;
|
|
57
|
+
}
|
|
58
|
+
if (nextDelta === 0) cancelScroll(event);
|
|
59
|
+
};
|
|
60
|
+
const onWheel = (event: WheelEvent) => handleDelta(event, event.deltaY);
|
|
61
|
+
const onTouchStart = (event: TouchEvent) => {
|
|
62
|
+
touchStart.current = event.changedTouches[0]?.clientY ?? 0;
|
|
63
|
+
};
|
|
64
|
+
const onTouchMove = (event: TouchEvent) => {
|
|
65
|
+
const next = event.changedTouches[0]?.clientY ?? touchStart.current;
|
|
66
|
+
handleDelta(event, touchStart.current - next);
|
|
67
|
+
};
|
|
68
|
+
element.addEventListener('wheel', onWheel, { passive: false });
|
|
69
|
+
element.addEventListener('touchstart', onTouchStart, { passive: false });
|
|
70
|
+
element.addEventListener('touchmove', onTouchMove, { passive: false });
|
|
71
|
+
return () => {
|
|
72
|
+
element.removeEventListener('wheel', onWheel);
|
|
73
|
+
element.removeEventListener('touchstart', onTouchStart);
|
|
74
|
+
element.removeEventListener('touchmove', onTouchMove);
|
|
75
|
+
};
|
|
76
|
+
}, [
|
|
77
|
+
options.captureEnabled,
|
|
78
|
+
options.enabled,
|
|
79
|
+
options.onBottomArrive,
|
|
80
|
+
options.onTopArrive,
|
|
81
|
+
options.target,
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (!options.enabled || !options.lockEnabled || typeof document === 'undefined') return;
|
|
86
|
+
const style = document.body.style;
|
|
87
|
+
if (activeScrollLocks === 0) {
|
|
88
|
+
originalBodyStyle = {
|
|
89
|
+
boxSizing: style.boxSizing,
|
|
90
|
+
height: style.height,
|
|
91
|
+
overflow: style.overflow,
|
|
92
|
+
paddingRight: style.paddingRight,
|
|
93
|
+
position: style.position,
|
|
94
|
+
};
|
|
95
|
+
const currentPadding = Number.parseInt(originalBodyStyle.paddingRight, 10) || 0;
|
|
96
|
+
const scrollbar = window.innerWidth - document.body.clientWidth;
|
|
97
|
+
style.boxSizing = 'border-box';
|
|
98
|
+
style.height = '100%';
|
|
99
|
+
style.overflow = 'hidden';
|
|
100
|
+
style.position = 'relative';
|
|
101
|
+
style.paddingRight = `${scrollbar + currentPadding}px`;
|
|
102
|
+
}
|
|
103
|
+
activeScrollLocks += 1;
|
|
104
|
+
return () => {
|
|
105
|
+
activeScrollLocks = Math.max(activeScrollLocks - 1, 0);
|
|
106
|
+
if (activeScrollLocks === 0 && originalBodyStyle) {
|
|
107
|
+
Object.assign(style, originalBodyStyle);
|
|
108
|
+
originalBodyStyle = null;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}, [options.enabled, options.lockEnabled]);
|
|
112
|
+
}
|