@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.
Files changed (37) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/package.json +1 -2
  3. package/src/button/select-button.tsx +11 -3
  4. package/src/collapsible/index.tsx +2 -2
  5. package/src/command-menu/index.tsx +4 -2
  6. package/src/copy-to-clipboard.ts +10 -0
  7. package/src/file-item-2.tsx +218 -128
  8. package/src/file-item.tsx +2 -0
  9. package/src/guide-link.tsx +1 -1
  10. package/src/html-styles.ts +2 -12
  11. package/src/icons/ast/FileImage.ts +1 -1
  12. package/src/icons/ast/Mermaid.ts +8 -0
  13. package/src/icons/ast/TextNoWrap.ts +8 -0
  14. package/src/icons/ast/TextWrap.ts +8 -0
  15. package/src/icons/ast/TypeId.ts +8 -0
  16. package/src/icons/ast/index.tsx +4 -0
  17. package/src/icons/react/Mermaid.tsx +13 -0
  18. package/src/icons/react/TextNoWrap.tsx +13 -0
  19. package/src/icons/react/TextWrap.tsx +13 -0
  20. package/src/icons/react/TypeId.tsx +13 -0
  21. package/src/icons/react/index.tsx +4 -0
  22. package/src/icons/svg/file-image.svg +2 -2
  23. package/src/icons/svg/mermaid.svg +3 -0
  24. package/src/icons/svg/text-no-wrap.svg +3 -0
  25. package/src/icons/svg/text-wrap.svg +3 -0
  26. package/src/icons/svg/type/id.svg +3 -0
  27. package/src/images-gallery/images-gallery-fall-through-provider.tsx +2 -1
  28. package/src/images-gallery/images-gallery.tsx +524 -185
  29. package/src/{images-gallery-2 → images-gallery}/slide-buttons.tsx +25 -13
  30. package/src/images-gallery/zoom.tsx +5 -94
  31. package/src/popover/index.tsx +5 -3
  32. package/src/select/components/menu-list-virtualized.tsx +123 -84
  33. package/src/select/components/menu.tsx +5 -3
  34. package/src/table.tsx +13 -10
  35. package/src/toggle-button/toggle-button.tsx +5 -0
  36. package/src/images-gallery-2/images-gallery-2.tsx +0 -522
  37. 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 {copyToClipboard} from "../copy-to-clipboard";
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={<ActionsButtonCompact size="medium" color="neutral" variant="soft" onClick={uiHandlers.showUI} />}
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
- copyToClipboard(src)
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 const ZoomV1 = (props: Props) => {
84
- const {zoomed, onZoomedChange, children, className, openOnClick, zoomImg} = props;
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
- <Controlled
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
+ }
@@ -121,9 +121,11 @@ export function MobilePopup({
121
121
  >
122
122
  {contextTitle ?? title}
123
123
  </div>
124
- <IconButton onClick={onClose} size="xLarge">
125
- <CloseIcon />
126
- </IconButton>
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 {Children, ComponentType, CSSProperties, ReactNode, useContext, useEffect, useRef, useState} from "react";
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
- const OPTION_HEIGHT = layout.menuItemHeight;
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
- const flattenChildren = flattenGroupChildren(children, renderGroup);
108
- //fix for select not having focused option when open. if no focused item present, at least first option must be focused.
109
- //Unfortunately, we can not rely on select focusedOption to determine option that should be focused initially:
110
- //to mark selected option react-select uses value field of Option object.
111
- //to define initially focused option react select uses whole Option object reference equality which is not always the case
112
- //if select value is not exactly same object as option, we end up having initially focusedOption being just first option.
113
- //fix below ensures that initial focusedOption is always selectedOption in cost of one more rerender
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
- //if an options exists, but there is no focused option, looking for first selected / existing option
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
- if (selectedOption) {
143
- setInitialFocusedOption(selectedOption as Option);
144
- select.current.setState({focusedOption: selectedOption});
145
- } else if (firstExistingOption) {
146
- setInitialFocusedOption(firstExistingOption as Option);
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
- const menuHeight = Math.min(flattenChildren.length * layout.menuItemHeight + PADDING_BLOCK_HEIGHT * 2, maxHeight);
152
- //scrolling to focused option
173
+
174
+ // Handle scroll to focused option on keyboard navigation
153
175
  useEffect(() => {
154
- let to: number | undefined;
155
- if (initialFocusedOption && focusedOption && virtuoso.current) {
156
- const index = flattenChildren.findIndex((child) => {
157
- if (isObject(child) && "props" in child) {
158
- if (isObject(child.props) && "data" in child.props) {
159
- return child.props.data === focusedOption;
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
- return false;
163
- });
164
- if (index >= 0) {
165
- const focusedOptionPosition = index * OPTION_HEIGHT;
189
+
166
190
  virtuoso.current.getState((state) => {
167
- to = window.setTimeout(() => {
168
- if (!virtuoso.current) {
169
- return;
170
- }
171
- if (state.scrollTop - SCROLL_INTO_VIEW_CHECK_FIX > focusedOptionPosition) {
172
- virtuoso.current.scrollToIndex({index: index, behavior: "auto"});
173
- } else if (
174
- state.scrollTop + menuHeight <
175
- focusedOptionPosition + OPTION_HEIGHT - SCROLL_INTO_VIEW_CHECK_FIX
176
- ) {
177
- virtuoso.current.scrollToIndex({
178
- index: index,
179
- align: "end",
180
- behavior: "auto",
181
- });
182
- }
183
- }, 1);
184
- // this just makes it smooth. Because of constant rerendering (or IDK why) Virtuoso state and actual scroll
185
- // value desync, like actual element do not scroll last frame, this stabilises the behaviour and force sync it
186
- virtuoso.current?.scrollTo({top: state.scrollTop});
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
- window.clearTimeout(to);
227
+ if (scrollTimeoutRef.current) {
228
+ window.cancelAnimationFrame(scrollTimeoutRef.current);
229
+ scrollTimeoutRef.current = undefined;
230
+ }
192
231
  };
193
- }, [focusedOption, flattenChildren, menuHeight, initialFocusedOption, virtuoso]);
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 HTMLDiveElement | null will appear here. We do not use Virtuoso's useWindowScroll prop
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={isPhone ? layout.mobileMenuItemHeight : layout.menuItemHeight}
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
- paddingTop: "",
15
- paddingBottom: "",
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
- display: "grid",
25
- gridTemplateColumns: "1fr minmax(140px, 7fr) 3fr minmax(140px, 3fr) 32px",
26
- borderRadius: border.radius6,
27
- padding: space.s6,
28
- paddingLeft: space.s12,
29
- marginBottom: space.s8,
30
- alignItems: "center",
31
- color: themeVars.textColor,
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