@fibery/ui-kit 1.39.1 → 1.40.0
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/CHANGELOG.md +6 -0
- package/package.json +1 -2
- package/src/button/select-button.tsx +11 -3
- package/src/collapsible/index.tsx +2 -2
- package/src/command-menu/index.tsx +4 -2
- package/src/copy-to-clipboard.ts +10 -0
- package/src/file-item-2.tsx +218 -128
- package/src/file-item.tsx +2 -0
- package/src/guide-link.tsx +1 -1
- package/src/html-styles.ts +2 -12
- package/src/icons/ast/FileImage.ts +1 -1
- package/src/icons/ast/Mermaid.ts +8 -0
- package/src/icons/ast/TextNoWrap.ts +8 -0
- package/src/icons/ast/TextWrap.ts +8 -0
- package/src/icons/ast/TypeId.ts +8 -0
- package/src/icons/ast/index.tsx +4 -0
- package/src/icons/react/Mermaid.tsx +13 -0
- package/src/icons/react/TextNoWrap.tsx +13 -0
- package/src/icons/react/TextWrap.tsx +13 -0
- package/src/icons/react/TypeId.tsx +13 -0
- package/src/icons/react/index.tsx +4 -0
- package/src/icons/svg/file-image.svg +2 -2
- package/src/icons/svg/mermaid.svg +3 -0
- package/src/icons/svg/text-no-wrap.svg +3 -0
- package/src/icons/svg/text-wrap.svg +3 -0
- package/src/icons/svg/type/id.svg +3 -0
- package/src/images-gallery/images-gallery-fall-through-provider.tsx +2 -1
- package/src/images-gallery/images-gallery.tsx +524 -185
- package/src/{images-gallery-2 → images-gallery}/slide-buttons.tsx +25 -13
- package/src/images-gallery/zoom.tsx +5 -94
- package/src/popover/index.tsx +5 -3
- package/src/select/components/menu-list-virtualized.tsx +123 -84
- package/src/select/components/menu.tsx +5 -3
- package/src/table.tsx +13 -10
- package/src/toggle-button/toggle-button.tsx +5 -0
- package/src/images-gallery-2/images-gallery-2.tsx +0 -522
- package/src/images-gallery-2/zoom.tsx +0 -35
|
@@ -12,13 +12,14 @@ import Close from "../icons/react/Close";
|
|
|
12
12
|
import {colors, createInlineTheme, getThemeColors, space} from "../design-system";
|
|
13
13
|
import {safeAreaInsetRightVar, safeAreaInsetTopVar} from "../mobile-styles";
|
|
14
14
|
import {Button} from "../button/button";
|
|
15
|
-
import {
|
|
15
|
+
import {copyUrlToClipboard} from "../copy-to-clipboard";
|
|
16
16
|
import {ToastProvider, useToast} from "../toast/toast-provider";
|
|
17
17
|
import {ToastQueue} from "../toast/toast-queue";
|
|
18
18
|
import {ActionsMenu} from "../actions-menu";
|
|
19
19
|
import {ActionsButtonCompact} from "../button/actions-button-compact";
|
|
20
20
|
import {ThemeProvider} from "../theme-provider";
|
|
21
21
|
import {ThemeMode} from "../theme-settings";
|
|
22
|
+
import {useIsPhone} from "../use-is-phone";
|
|
22
23
|
|
|
23
24
|
const barClassName = css`
|
|
24
25
|
position: absolute;
|
|
@@ -79,18 +80,20 @@ export function SlideButtons({
|
|
|
79
80
|
return {theme, styles: createInlineTheme(theme, emptyArray)};
|
|
80
81
|
}, []);
|
|
81
82
|
|
|
83
|
+
const isPhone = useIsPhone();
|
|
84
|
+
|
|
82
85
|
return (
|
|
83
86
|
<ToastProvider toastQueue={toastQueue}>
|
|
84
87
|
<ThemeProvider theme={theme}>
|
|
85
88
|
<div style={styles} className={wrapper}>
|
|
86
89
|
<div className={barClassName}>
|
|
87
|
-
<CopyURLButton pswp={pswp} showUI={uiHandlers.showUI} />
|
|
90
|
+
{slideState.src ? <CopyURLButton pswp={pswp} showUI={uiHandlers.showUI} /> : null}
|
|
88
91
|
<ZoomButton pswp={pswp} showUI={uiHandlers.showUI} />
|
|
89
92
|
{slideState.actions ? (
|
|
90
93
|
<SlideActionsMenu actions={slideState.actions} uiHandlers={uiHandlers} />
|
|
91
|
-
) : (
|
|
94
|
+
) : slideState.src ? (
|
|
92
95
|
<IconButton
|
|
93
|
-
size="medium"
|
|
96
|
+
size={isPhone ? "large" : "medium"}
|
|
94
97
|
color="neutral"
|
|
95
98
|
variant="soft"
|
|
96
99
|
onClick={() => {
|
|
@@ -100,14 +103,14 @@ export function SlideButtons({
|
|
|
100
103
|
>
|
|
101
104
|
<RicheditorOpenLink />
|
|
102
105
|
</IconButton>
|
|
103
|
-
)}
|
|
106
|
+
) : null}
|
|
104
107
|
|
|
105
108
|
<IconButton
|
|
106
109
|
onClick={() => {
|
|
107
110
|
pswp.close();
|
|
108
111
|
uiHandlers.showUI();
|
|
109
112
|
}}
|
|
110
|
-
size="medium"
|
|
113
|
+
size={isPhone ? "large" : "medium"}
|
|
111
114
|
color="neutral"
|
|
112
115
|
variant="soft"
|
|
113
116
|
>
|
|
@@ -121,7 +124,7 @@ export function SlideButtons({
|
|
|
121
124
|
pswp.prev();
|
|
122
125
|
uiHandlers.showUI();
|
|
123
126
|
}}
|
|
124
|
-
size="medium"
|
|
127
|
+
size={isPhone ? "large" : "medium"}
|
|
125
128
|
color="neutral"
|
|
126
129
|
variant="soft"
|
|
127
130
|
>
|
|
@@ -135,7 +138,7 @@ export function SlideButtons({
|
|
|
135
138
|
pswp.next();
|
|
136
139
|
uiHandlers.showUI();
|
|
137
140
|
}}
|
|
138
|
-
size="medium"
|
|
141
|
+
size={isPhone ? "large" : "medium"}
|
|
139
142
|
color="neutral"
|
|
140
143
|
variant="soft"
|
|
141
144
|
>
|
|
@@ -156,7 +159,7 @@ function SlideActionsMenu({
|
|
|
156
159
|
uiHandlers: {showUI: () => void; setForceShowUI: (value: boolean) => void};
|
|
157
160
|
}) {
|
|
158
161
|
const [container, setContainer] = useState<HTMLDivElement | null>();
|
|
159
|
-
|
|
162
|
+
const isPhone = useIsPhone();
|
|
160
163
|
return (
|
|
161
164
|
<div>
|
|
162
165
|
<ActionsMenu
|
|
@@ -167,7 +170,14 @@ function SlideActionsMenu({
|
|
|
167
170
|
margin-top: ${space.s8}px;
|
|
168
171
|
`}
|
|
169
172
|
container={container}
|
|
170
|
-
trigger={
|
|
173
|
+
trigger={
|
|
174
|
+
<ActionsButtonCompact
|
|
175
|
+
size={isPhone ? "large" : "medium"}
|
|
176
|
+
color="neutral"
|
|
177
|
+
variant="soft"
|
|
178
|
+
onClick={uiHandlers.showUI}
|
|
179
|
+
/>
|
|
180
|
+
}
|
|
171
181
|
>
|
|
172
182
|
{actions}
|
|
173
183
|
</ActionsMenu>
|
|
@@ -179,6 +189,7 @@ function SlideActionsMenu({
|
|
|
179
189
|
function ZoomButton({pswp, showUI}: {pswp: PhotoSwipe; showUI: () => void}) {
|
|
180
190
|
const [zoomed, setZoomed] = useState(false);
|
|
181
191
|
const [isVisible, setIsVisible] = useState(false);
|
|
192
|
+
const isPhone = useIsPhone();
|
|
182
193
|
|
|
183
194
|
useEffect(() => {
|
|
184
195
|
if (!pswp) {
|
|
@@ -217,7 +228,7 @@ function ZoomButton({pswp, showUI}: {pswp: PhotoSwipe; showUI: () => void}) {
|
|
|
217
228
|
pswp.toggleZoom();
|
|
218
229
|
showUI();
|
|
219
230
|
}}
|
|
220
|
-
size="medium"
|
|
231
|
+
size={isPhone ? "large" : "medium"}
|
|
221
232
|
color="neutral"
|
|
222
233
|
variant="soft"
|
|
223
234
|
>
|
|
@@ -228,6 +239,7 @@ function ZoomButton({pswp, showUI}: {pswp: PhotoSwipe; showUI: () => void}) {
|
|
|
228
239
|
|
|
229
240
|
function CopyURLButton({pswp, showUI}: {pswp: PhotoSwipe; showUI: () => void}) {
|
|
230
241
|
const toast = useToast();
|
|
242
|
+
const isPhone = useIsPhone();
|
|
231
243
|
return (
|
|
232
244
|
<Button
|
|
233
245
|
onClick={() => {
|
|
@@ -239,7 +251,7 @@ function CopyURLButton({pswp, showUI}: {pswp: PhotoSwipe; showUI: () => void}) {
|
|
|
239
251
|
|
|
240
252
|
const src = new URL(pswp.currSlide.data.src, `${window.location}`).href;
|
|
241
253
|
if (src) {
|
|
242
|
-
|
|
254
|
+
copyUrlToClipboard({url: src, label: pswp.currSlide.data.label})
|
|
243
255
|
.then(() => {
|
|
244
256
|
toast.success({title: "Copied!"});
|
|
245
257
|
})
|
|
@@ -248,7 +260,7 @@ function CopyURLButton({pswp, showUI}: {pswp: PhotoSwipe; showUI: () => void}) {
|
|
|
248
260
|
});
|
|
249
261
|
}
|
|
250
262
|
}}
|
|
251
|
-
size="medium"
|
|
263
|
+
size={isPhone ? "large" : "medium"}
|
|
252
264
|
variant="soft"
|
|
253
265
|
color="neutral"
|
|
254
266
|
iconStart={<Link />}
|
|
@@ -1,75 +1,4 @@
|
|
|
1
|
-
import {css} from "@linaria/core";
|
|
2
1
|
import React, {ReactNode, useCallback} from "react";
|
|
3
|
-
import {Controlled} from "react-medium-image-zoom";
|
|
4
|
-
import "react-medium-image-zoom/dist/styles.css";
|
|
5
|
-
import {themeVars} from "../design-system";
|
|
6
|
-
import {IconButton} from "../button/icon-button";
|
|
7
|
-
import Close from "../icons/react/Close";
|
|
8
|
-
import {Controls, useImagesGalleryMethods, useImagesGalleryState} from "./images-gallery";
|
|
9
|
-
import {useIsPhone} from "../use-is-phone";
|
|
10
|
-
import {ZoomV2} from "../images-gallery-2/zoom";
|
|
11
|
-
|
|
12
|
-
const dialogCss = css`
|
|
13
|
-
:global() {
|
|
14
|
-
[data-rmiz-modal-overlay="hidden"] {
|
|
15
|
-
background-color: transparent;
|
|
16
|
-
}
|
|
17
|
-
[data-rmiz-modal-overlay="visible"] {
|
|
18
|
-
background-color: ${themeVars.colorBgEditorImageZoomed};
|
|
19
|
-
}
|
|
20
|
-
[data-rmiz-content="found"] > img {
|
|
21
|
-
cursor: auto;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
`;
|
|
25
|
-
|
|
26
|
-
type ModalState = "LOADED" | "LOADING" | "UNLOADED" | "UNLOADING";
|
|
27
|
-
|
|
28
|
-
const ZoomContent = ({
|
|
29
|
-
img,
|
|
30
|
-
modalState,
|
|
31
|
-
onUnzoom,
|
|
32
|
-
}: {
|
|
33
|
-
img: React.ReactElement | null;
|
|
34
|
-
modalState: ModalState;
|
|
35
|
-
onUnzoom: (e: Event) => void;
|
|
36
|
-
}) => {
|
|
37
|
-
const imagesGalleryMethods = useImagesGalleryMethods();
|
|
38
|
-
const isPhone = useIsPhone();
|
|
39
|
-
|
|
40
|
-
const button = modalState === "LOADED" && (
|
|
41
|
-
<IconButton
|
|
42
|
-
size={isPhone ? "xLarge" : "small"}
|
|
43
|
-
color="neutral"
|
|
44
|
-
variant="ghost"
|
|
45
|
-
className={css`
|
|
46
|
-
position: absolute;
|
|
47
|
-
inset: 20px 20px auto auto;
|
|
48
|
-
z-index: 1;
|
|
49
|
-
`}
|
|
50
|
-
onClick={onUnzoom as () => void}
|
|
51
|
-
>
|
|
52
|
-
<Close />
|
|
53
|
-
</IconButton>
|
|
54
|
-
);
|
|
55
|
-
return (
|
|
56
|
-
<>
|
|
57
|
-
<div
|
|
58
|
-
onMouseMove={() => {
|
|
59
|
-
imagesGalleryMethods.setVisible(true);
|
|
60
|
-
}}
|
|
61
|
-
className={css`
|
|
62
|
-
position: absolute;
|
|
63
|
-
inset: 0;
|
|
64
|
-
`}
|
|
65
|
-
>
|
|
66
|
-
{img}
|
|
67
|
-
{button}
|
|
68
|
-
</div>
|
|
69
|
-
<Controls />
|
|
70
|
-
</>
|
|
71
|
-
);
|
|
72
|
-
};
|
|
73
2
|
|
|
74
3
|
type Props = {
|
|
75
4
|
zoomed: boolean;
|
|
@@ -78,11 +7,11 @@ type Props = {
|
|
|
78
7
|
className?: string;
|
|
79
8
|
openOnClick?: boolean;
|
|
80
9
|
zoomImg?: {src: string; alt?: string};
|
|
10
|
+
actions?: ReactNode;
|
|
81
11
|
};
|
|
82
12
|
|
|
83
|
-
export
|
|
84
|
-
const {zoomed, onZoomedChange, children, className, openOnClick
|
|
85
|
-
const imagesGalleryMethods = useImagesGalleryMethods();
|
|
13
|
+
export function Zoom(props: Props) {
|
|
14
|
+
const {zoomed, onZoomedChange, children, className, openOnClick} = props;
|
|
86
15
|
const onClick = useCallback(() => {
|
|
87
16
|
if (zoomed) {
|
|
88
17
|
onZoomedChange(false);
|
|
@@ -99,25 +28,7 @@ export const ZoomV1 = (props: Props) => {
|
|
|
99
28
|
|
|
100
29
|
return (
|
|
101
30
|
<div className={className} onClick={onClick} onDoubleClick={openOnClick ? undefined : onDoubleClick}>
|
|
102
|
-
|
|
103
|
-
zoomImg={zoomImg}
|
|
104
|
-
onZoomChange={imagesGalleryMethods.update}
|
|
105
|
-
classDialog={dialogCss}
|
|
106
|
-
isZoomed={zoomed}
|
|
107
|
-
ZoomContent={ZoomContent}
|
|
108
|
-
>
|
|
109
|
-
{children}
|
|
110
|
-
</Controlled>
|
|
31
|
+
{children}
|
|
111
32
|
</div>
|
|
112
33
|
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export const Zoom = (props: Props) => {
|
|
116
|
-
const {version} = useImagesGalleryState();
|
|
117
|
-
|
|
118
|
-
if (version === "1") {
|
|
119
|
-
return <ZoomV1 {...props} />;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return <ZoomV2 {...props} />;
|
|
123
|
-
};
|
|
34
|
+
}
|
package/src/popover/index.tsx
CHANGED
|
@@ -121,9 +121,11 @@ export function MobilePopup({
|
|
|
121
121
|
>
|
|
122
122
|
{contextTitle ?? title}
|
|
123
123
|
</div>
|
|
124
|
-
|
|
125
|
-
<
|
|
126
|
-
|
|
124
|
+
{onClose && (
|
|
125
|
+
<IconButton onClick={onClose} size="xLarge">
|
|
126
|
+
<CloseIcon />
|
|
127
|
+
</IconButton>
|
|
128
|
+
)}
|
|
127
129
|
</>
|
|
128
130
|
);
|
|
129
131
|
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import {css} from "@linaria/core";
|
|
2
2
|
import _, {isFunction, isObject} from "lodash";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
Children,
|
|
5
|
+
ComponentType,
|
|
6
|
+
CSSProperties,
|
|
7
|
+
ReactNode,
|
|
8
|
+
useContext,
|
|
9
|
+
useEffect,
|
|
10
|
+
useMemo,
|
|
11
|
+
useRef,
|
|
12
|
+
useState,
|
|
13
|
+
} from "react";
|
|
4
14
|
import {GroupHeadingProps, GroupProps, MenuListProps} from "react-select";
|
|
5
15
|
import {ItemContent, Virtuoso, VirtuosoHandle} from "react-virtuoso";
|
|
6
16
|
import {layout} from "../../../src/design-system";
|
|
@@ -23,12 +33,8 @@ const components = {
|
|
|
23
33
|
Header: Spacer,
|
|
24
34
|
};
|
|
25
35
|
|
|
26
|
-
|
|
27
|
-
// this code is logic stolen from windowed-select SCROLL_INTO_VIEW_CHECK_FIX prevents list scrolling when focusing
|
|
28
|
-
// partially visible option. set to 0 to scroll into view any partially hidden focused option every time
|
|
29
|
-
// windowed select default is .33. We've got some precision here, can go with .2
|
|
36
|
+
// Threshold to determine when to scroll: if focused item is less than this visible, scroll it into view
|
|
30
37
|
const MINIMAL_VISIBLE_PORTION_TO_SCROLL_INTO_VIEW = 0.2;
|
|
31
|
-
const SCROLL_INTO_VIEW_CHECK_FIX = OPTION_HEIGHT - OPTION_HEIGHT * MINIMAL_VISIBLE_PORTION_TO_SCROLL_INTO_VIEW;
|
|
32
38
|
function isGroupChild<Option, isMulty extends boolean, Group extends GroupBase<Option> = GroupBase<Option>>(
|
|
33
39
|
props: unknown
|
|
34
40
|
): props is GroupProps<Option, isMulty, Group> {
|
|
@@ -77,45 +83,64 @@ export function MenuListVirtualized<
|
|
|
77
83
|
const isPhone = useIsPhone();
|
|
78
84
|
const {inPopover} = useSelectControlSettings();
|
|
79
85
|
const virtuoso = useRef<VirtuosoHandle>(null);
|
|
80
|
-
|
|
81
|
-
// options props come non-filtered, and we have insufficient info (props) to render groups and options from here.
|
|
82
|
-
// Also, you never know what contents also possible as list content.
|
|
83
|
-
// that's why it is better working with children and find options and groups among them (hope internal API does not change)
|
|
84
|
-
// The best strategy to scroll to index and to evade height calculations is to
|
|
85
|
-
// deconstruct group into it's header and children (options) and render then side by side without group-wrapping div
|
|
86
|
-
// then we just use integrated scrollToIndex fromVirtuoso API to scroll to desired option (most reliable variant)
|
|
87
|
-
// that's why Group is just a dummy component that does nothing: to minimize impact of Group component being rendered inside ReactSelect
|
|
88
|
-
const renderGroup = (Component: ComponentType<GroupHeadingProps>, headingProps: GroupProps["headingProps"]) => {
|
|
89
|
-
const content = selectProps.formatGroupLabel
|
|
90
|
-
? selectProps.formatGroupLabel(headingProps.data as Group)
|
|
91
|
-
: headingProps.data.label;
|
|
92
|
-
return content ? (
|
|
93
|
-
<Component
|
|
94
|
-
cx={cx}
|
|
95
|
-
selectProps={selectProps as unknown as GroupHeadingProps["selectProps"]}
|
|
96
|
-
theme={theme}
|
|
97
|
-
getStyles={getStyles}
|
|
98
|
-
{...headingProps}
|
|
99
|
-
>
|
|
100
|
-
{content}
|
|
101
|
-
</Component>
|
|
102
|
-
) : null;
|
|
103
|
-
};
|
|
104
86
|
const select = useContext(ReactSelectRefContext);
|
|
105
87
|
const [initialFocusedOption, setInitialFocusedOption] = useState<Option | null>(null);
|
|
88
|
+
const scrollTimeoutRef = useRef<number | undefined>();
|
|
89
|
+
|
|
90
|
+
// Get the appropriate item height based on device
|
|
91
|
+
const itemHeight = isPhone ? layout.mobileMenuItemHeight : layout.menuItemHeight;
|
|
92
|
+
const scrollThreshold = itemHeight * MINIMAL_VISIBLE_PORTION_TO_SCROLL_INTO_VIEW;
|
|
93
|
+
|
|
94
|
+
// Render group heading
|
|
95
|
+
const renderGroup = useMemo(
|
|
96
|
+
() => (Component: ComponentType<GroupHeadingProps>, headingProps: GroupProps["headingProps"]) => {
|
|
97
|
+
const content = selectProps.formatGroupLabel
|
|
98
|
+
? selectProps.formatGroupLabel(headingProps.data as Group)
|
|
99
|
+
: headingProps.data.label;
|
|
100
|
+
return content ? (
|
|
101
|
+
<Component
|
|
102
|
+
cx={cx}
|
|
103
|
+
selectProps={selectProps as unknown as GroupHeadingProps["selectProps"]}
|
|
104
|
+
theme={theme}
|
|
105
|
+
getStyles={getStyles}
|
|
106
|
+
{...headingProps}
|
|
107
|
+
>
|
|
108
|
+
{content}
|
|
109
|
+
</Component>
|
|
110
|
+
) : null;
|
|
111
|
+
},
|
|
112
|
+
[cx, getStyles, selectProps, theme]
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Flatten children to handle groups
|
|
116
|
+
const flattenChildren = useMemo(() => flattenGroupChildren(children, renderGroup), [children, renderGroup]);
|
|
117
|
+
|
|
118
|
+
// Calculate menu height
|
|
119
|
+
const menuHeight = useMemo(
|
|
120
|
+
() => Math.min(flattenChildren.length * itemHeight + PADDING_BLOCK_HEIGHT * 2, maxHeight),
|
|
121
|
+
[flattenChildren.length, itemHeight, maxHeight]
|
|
122
|
+
);
|
|
106
123
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
124
|
+
// Find index of focused option in flattened children
|
|
125
|
+
const focusedIndex = useMemo(() => {
|
|
126
|
+
if (!focusedOption) {
|
|
127
|
+
return -1;
|
|
128
|
+
}
|
|
129
|
+
return flattenChildren.findIndex((child) => {
|
|
130
|
+
if (isObject(child) && "props" in child && isObject(child.props) && "data" in child.props) {
|
|
131
|
+
return child.props.data === focusedOption;
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
});
|
|
135
|
+
}, [focusedOption, flattenChildren]);
|
|
136
|
+
|
|
137
|
+
// Initialize focused option on mount
|
|
114
138
|
useEffect(() => {
|
|
115
139
|
if (select.current && flattenChildren.length && !initialFocusedOption) {
|
|
116
140
|
let firstExistingOption: unknown;
|
|
117
141
|
let selectedOption: unknown;
|
|
118
|
-
|
|
142
|
+
|
|
143
|
+
// Find first selected option or first available option
|
|
119
144
|
for (let i = 0; i < flattenChildren.length; i++) {
|
|
120
145
|
const child = flattenChildren[i];
|
|
121
146
|
if (
|
|
@@ -126,10 +151,8 @@ export function MenuListVirtualized<
|
|
|
126
151
|
child.props.type === "option" &&
|
|
127
152
|
"isSelected" in child.props
|
|
128
153
|
) {
|
|
129
|
-
//well it is an option, check if it has data (just for ts and be sure)
|
|
130
154
|
if ("data" in child.props && isObject(child.props.data)) {
|
|
131
155
|
if ("isSelected" in child.props && child.props.isSelected) {
|
|
132
|
-
//setting selected option data as focused option, wrapping up
|
|
133
156
|
selectedOption = child.props.data;
|
|
134
157
|
break;
|
|
135
158
|
}
|
|
@@ -139,68 +162,84 @@ export function MenuListVirtualized<
|
|
|
139
162
|
}
|
|
140
163
|
}
|
|
141
164
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
select.current.setState({focusedOption: firstExistingOption});
|
|
165
|
+
|
|
166
|
+
const optionToFocus = selectedOption || firstExistingOption;
|
|
167
|
+
if (optionToFocus) {
|
|
168
|
+
setInitialFocusedOption(optionToFocus as Option);
|
|
169
|
+
select.current.setState({focusedOption: optionToFocus});
|
|
148
170
|
}
|
|
149
171
|
}
|
|
150
172
|
}, [flattenChildren, initialFocusedOption, select]);
|
|
151
|
-
|
|
152
|
-
//
|
|
173
|
+
|
|
174
|
+
// Handle scroll to focused option on keyboard navigation
|
|
153
175
|
useEffect(() => {
|
|
154
|
-
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
176
|
+
// Clear any pending scroll timeout
|
|
177
|
+
if (scrollTimeoutRef.current) {
|
|
178
|
+
window.cancelAnimationFrame(scrollTimeoutRef.current);
|
|
179
|
+
scrollTimeoutRef.current = undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Only scroll if we have an initial focused option and current focused option
|
|
183
|
+
if (initialFocusedOption && focusedOption && focusedIndex >= 0 && virtuoso.current) {
|
|
184
|
+
// Use requestAnimationFrame for smooth scrolling aligned with browser rendering
|
|
185
|
+
scrollTimeoutRef.current = window.requestAnimationFrame(() => {
|
|
186
|
+
if (!virtuoso.current) {
|
|
187
|
+
return;
|
|
161
188
|
}
|
|
162
|
-
|
|
163
|
-
});
|
|
164
|
-
if (index >= 0) {
|
|
165
|
-
const focusedOptionPosition = index * OPTION_HEIGHT;
|
|
189
|
+
|
|
166
190
|
virtuoso.current.getState((state) => {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
191
|
+
const focusedItemTop = focusedIndex * itemHeight + PADDING_BLOCK_HEIGHT;
|
|
192
|
+
const focusedItemBottom = focusedItemTop + itemHeight;
|
|
193
|
+
const viewportTop = state.scrollTop;
|
|
194
|
+
const viewportBottom = state.scrollTop + menuHeight - PADDING_BLOCK_HEIGHT;
|
|
195
|
+
|
|
196
|
+
// Check if item is cut off at the top (even partially)
|
|
197
|
+
const isTopCutOff = focusedItemTop < viewportTop;
|
|
198
|
+
// Check if item is cut off at the bottom (even partially)
|
|
199
|
+
const isBottomCutOff = focusedItemBottom > viewportBottom;
|
|
200
|
+
|
|
201
|
+
// Calculate how much of the item is visible
|
|
202
|
+
const visibleTop = Math.max(focusedItemTop, viewportTop);
|
|
203
|
+
const visibleBottom = Math.min(focusedItemBottom, viewportBottom);
|
|
204
|
+
const visibleHeight = Math.max(0, visibleBottom - visibleTop);
|
|
205
|
+
|
|
206
|
+
// Scroll if item is cut off and not sufficiently visible
|
|
207
|
+
if (isTopCutOff && visibleHeight < itemHeight - scrollThreshold) {
|
|
208
|
+
// Item is cut off at the top and less than threshold is hidden
|
|
209
|
+
virtuoso.current?.scrollToIndex({
|
|
210
|
+
index: focusedIndex,
|
|
211
|
+
align: "start",
|
|
212
|
+
behavior: "auto",
|
|
213
|
+
});
|
|
214
|
+
} else if (isBottomCutOff && visibleHeight < itemHeight - scrollThreshold) {
|
|
215
|
+
// Item is cut off at the bottom and less than threshold is hidden
|
|
216
|
+
virtuoso.current?.scrollToIndex({
|
|
217
|
+
index: focusedIndex,
|
|
218
|
+
align: "end",
|
|
219
|
+
behavior: "auto",
|
|
220
|
+
});
|
|
221
|
+
}
|
|
187
222
|
});
|
|
188
|
-
}
|
|
223
|
+
}) as unknown as number;
|
|
189
224
|
}
|
|
225
|
+
|
|
190
226
|
return () => {
|
|
191
|
-
|
|
227
|
+
if (scrollTimeoutRef.current) {
|
|
228
|
+
window.cancelAnimationFrame(scrollTimeoutRef.current);
|
|
229
|
+
scrollTimeoutRef.current = undefined;
|
|
230
|
+
}
|
|
192
231
|
};
|
|
193
|
-
}, [focusedOption,
|
|
232
|
+
}, [focusedOption, focusedIndex, initialFocusedOption, itemHeight, menuHeight, scrollThreshold]);
|
|
194
233
|
|
|
195
234
|
return (
|
|
196
235
|
// Needed for correct display inside portals and etc.
|
|
197
236
|
<div style={getStyles("menuList", props) as CSSProperties}>
|
|
198
237
|
<Virtuoso
|
|
199
238
|
ref={virtuoso}
|
|
200
|
-
// in reality only
|
|
239
|
+
// in reality only HTMLDivElement | null will appear here. We do not use Virtuoso's useWindowScroll prop
|
|
201
240
|
scrollerRef={innerRef as (ref: HTMLElement | Window | null) => void}
|
|
202
241
|
style={isPhone && inPopover ? undefined : {height: menuHeight}}
|
|
203
|
-
fixedItemHeight={
|
|
242
|
+
fixedItemHeight={itemHeight}
|
|
204
243
|
data={flattenChildren}
|
|
205
244
|
totalCount={flattenChildren.length}
|
|
206
245
|
itemContent={drawListElement}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {css, cx} from "@linaria/core";
|
|
2
2
|
import {CSSObjectWithLabel, MenuProps, components} from "react-select";
|
|
3
|
-
import {border, themeVars} from "../../design-system";
|
|
3
|
+
import {border, space, themeVars} from "../../design-system";
|
|
4
4
|
import {GroupBase} from "../index";
|
|
5
5
|
|
|
6
6
|
export const MenuListNulledStyles: CSSObjectWithLabel = {
|
|
@@ -11,8 +11,10 @@ export const MenuNulledStyles: CSSObjectWithLabel = {
|
|
|
11
11
|
backgroundColor: "",
|
|
12
12
|
borderRadius: "",
|
|
13
13
|
boxShadow: "",
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
marginTop: 0,
|
|
15
|
+
marginBottom: 0,
|
|
16
|
+
paddingTop: space.s8,
|
|
17
|
+
paddingBottom: space.s8,
|
|
16
18
|
};
|
|
17
19
|
|
|
18
20
|
const menuCss = css`
|
package/src/table.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {PureComponent, ReactNode} from "react";
|
|
2
2
|
import {border, space, themeVars} from "./design-system";
|
|
3
3
|
import {css, cx} from "@linaria/core";
|
|
4
|
+
import {mobileRootSelector} from "./mobile-styles";
|
|
4
5
|
|
|
5
6
|
const tableStyle = css`
|
|
6
7
|
${{
|
|
@@ -18,18 +19,20 @@ export class Table extends PureComponent<{children: ReactNode}> {
|
|
|
18
19
|
return <div className={tableStyle}>{children}</div>;
|
|
19
20
|
}
|
|
20
21
|
}
|
|
22
|
+
const minWidthProperty = "--fibery-min-column-width";
|
|
21
23
|
|
|
22
24
|
const rowStyle = css`
|
|
23
|
-
${
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
${minWidthProperty}: 140px;
|
|
26
|
+
${mobileRootSelector} & {
|
|
27
|
+
${minWidthProperty}: 120px;
|
|
28
|
+
}
|
|
29
|
+
display: grid;
|
|
30
|
+
grid-template-columns: 1fr minmax(var(${minWidthProperty}), 7fr) 3fr minmax(var(${minWidthProperty}), 3fr) 32px;
|
|
31
|
+
border-radius: ${border.radius6}px;
|
|
32
|
+
padding: ${space.s6}px ${space.s6}px ${space.s12}px ${space.s6}px;
|
|
33
|
+
margin-bottom: ${space.s8}px;
|
|
34
|
+
align-items: center;
|
|
35
|
+
color: ${themeVars.textColor};
|
|
33
36
|
`;
|
|
34
37
|
|
|
35
38
|
const cellSingleLineStyle = css`
|
|
@@ -55,6 +55,11 @@ export const toggleButtonCss = css`
|
|
|
55
55
|
&:is(:active, [aria-expanded="true"]) {
|
|
56
56
|
background-color: ${themeVars.colorBgReactionsHover};
|
|
57
57
|
border-color: ${themeVars.colorBorderReactionsHover};
|
|
58
|
+
|
|
59
|
+
&:is([aria-pressed="true"], [aria-checked="true"]) {
|
|
60
|
+
background-color: ${themeVars.colorBgReactionsSelectedHover};
|
|
61
|
+
border-color: ${themeVars.colorBorderReactionsSelectedHover};
|
|
62
|
+
}
|
|
58
63
|
}
|
|
59
64
|
}
|
|
60
65
|
|