@ssa-ui-kit/core 3.3.0 → 3.4.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.
@@ -5,8 +5,13 @@ import { DropdownProps } from './types';
5
5
  *
6
6
  * A flexible dropdown component that allows users to select one option from
7
7
  * a list of choices. Uses a compound component pattern with DropdownOption
8
- * children. Provides keyboard navigation, accessibility features, and click-outside
9
- * to close functionality.
8
+ * children. Provides keyboard navigation, accessibility features, click-outside
9
+ * to close functionality, and automatic viewport-aware placement of the options list.
10
+ *
11
+ * On every open the component measures available space below the toggle button
12
+ * and flips the list upward when there is not enough room, preventing the list
13
+ * from being clipped by the viewport edge. This behavior can be overridden via
14
+ * dropdownProps.dropdownPosition.
10
15
  *
11
16
  * Component structure:
12
17
  * - Dropdown (root container with context)
@@ -57,14 +62,15 @@ import { DropdownProps } from './types';
57
62
  *
58
63
  * @example
59
64
  * ```tsx
60
- * // With custom props for sub-components
65
+ * // With custom props for sub-components and forced upward placement
61
66
  * <Dropdown
62
67
  * selectedItem={selected}
63
68
  * onChange={handleChange}
64
69
  * dropdownProps={{
65
70
  * base: { id: 'my-dropdown' },
66
71
  * toggleButton: { 'data-testid': 'dropdown-toggle' },
67
- * toggleButtonArrow: { className: 'custom-arrow' }
72
+ * toggleButtonArrow: { className: 'custom-arrow' },
73
+ * dropdownPosition: DropdownPositions.top,
68
74
  * }}
69
75
  * >
70
76
  * {options.map(opt => (
@@ -3,6 +3,11 @@ import { Interpolation, Theme } from '@emotion/react';
3
3
  import { DropdownOptionProps } from '../DropdownOptions';
4
4
  import { CommonProps } from '../../types/emotion';
5
5
  import { IconProps } from '../Icon/types';
6
+ export declare enum DropdownPositions {
7
+ top = "top",
8
+ bottom = "bottom",
9
+ auto = "auto"
10
+ }
6
11
  /**
7
12
  * Props that are controlled by Dropdown component
8
13
  * These props cannot be passed via dropdownProps.toggleButton
@@ -13,7 +18,8 @@ export type ControlledButtonProps = 'onClick' | 'onFocus' | 'disabled' | 'type'
13
18
  *
14
19
  * A select-like dropdown component that allows users to choose one option from
15
20
  * a list. Uses compound component pattern with DropdownOption children.
16
- * Provides keyboard navigation, accessibility, and customizable styling.
21
+ * Provides keyboard navigation, accessibility, customizable styling, and
22
+ * automatic viewport-aware placement of the options list.
17
23
  *
18
24
  * @example
19
25
  * ```tsx
@@ -34,6 +40,18 @@ export type ControlledButtonProps = 'onClick' | 'onFocus' | 'disabled' | 'type'
34
40
  * ))}
35
41
  * </Dropdown>
36
42
  * ```
43
+ *
44
+ * @example
45
+ * ```tsx
46
+ * // Force the list to always open upward (e.g. component near the bottom of the page)
47
+ * <Dropdown
48
+ * selectedItem={selected}
49
+ * onChange={handleChange}
50
+ * dropdownProps={{ dropdownPosition: DropdownPositions.top }}
51
+ * >
52
+ * ...
53
+ * </Dropdown>
54
+ * ```
37
55
  */
38
56
  export interface DropdownProps<P extends DropdownOptionProps> extends CommonProps {
39
57
  /**
@@ -109,6 +127,15 @@ export interface DropdownProps<P extends DropdownOptionProps> extends CommonProp
109
127
  };
110
128
  /** Props for the arrow icon SVG element */
111
129
  toggleButtonArrow?: Omit<IconProps, 'name' | 'size'>;
130
+ /**
131
+ * Controls the opening direction of the options list.
132
+ * - DropdownPositions.auto: opens downward by default; flips upward automatically when
133
+ * there is insufficient space below the toggle in the viewport
134
+ * - DropdownPositions.top: always opens upward
135
+ * - DropdownPositions.bottom: always opens downward
136
+ * @default DropdownPositions.auto
137
+ */
138
+ dropdownPosition?: DropdownPositions;
112
139
  };
113
140
  }
114
141
  /**
@@ -130,4 +157,14 @@ export interface DropdownContextType {
130
157
  * Max height (px) of the options list
131
158
  */
132
159
  maxHeight?: number;
160
+ /**
161
+ * Ref attached to the options list element
162
+ * Used by Dropdown to measure actual list height for placement calculation
163
+ */
164
+ listRef?: React.RefObject<HTMLUListElement | null>;
165
+ /**
166
+ * Resolved placement of the options list
167
+ * Computed by Dropdown based on dropdownPosition prop and available viewport space
168
+ */
169
+ placement?: 'top' | 'bottom';
133
170
  }
@@ -5,6 +5,11 @@ import { DropdownItemsListProps } from './types';
5
5
  * Renders the scrollable list of options that appears when the dropdown is open.
6
6
  * Provides proper ARIA attributes for accessibility and keyboard navigation.
7
7
  *
8
+ * Placement (opening upward or downward) is driven entirely by the parent
9
+ * Dropdown via context — this component does not calculate position itself.
10
+ * A ref is attached to the list element so Dropdown can measure its actual
11
+ * rendered height when determining the correct placement on each open.
12
+ *
8
13
  * @category Form Controls
9
14
  * @subcategory Selection
10
15
  *
@@ -40,4 +40,4 @@ import { RowsPerPageDropdownProps } from './types';
40
40
  * - Keyboard navigation support
41
41
  * - Screen reader friendly
42
42
  */
43
- export declare const RowsPerPageDropdown: ({ selectedItem, rowsPerPageList, rowsPerPageText, ...rest }: RowsPerPageDropdownProps) => import("@emotion/react/jsx-runtime").JSX.Element;
43
+ export declare const RowsPerPageDropdown: ({ selectedItem, rowsPerPageList, rowsPerPageText, dropdownPosition, ...rest }: RowsPerPageDropdownProps) => import("@emotion/react/jsx-runtime").JSX.Element;
@@ -1,4 +1,5 @@
1
1
  import { CommonProps } from '../../../../types/emotion';
2
+ import { DropdownPositions } from '../../../index';
2
3
  /**
3
4
  * Props for the RowsPerPageDropdown component
4
5
  *
@@ -45,4 +46,13 @@ export interface RowsPerPageDropdownProps extends CommonProps {
45
46
  id: number;
46
47
  value: number;
47
48
  }>;
49
+ /**
50
+ * Controls the opening direction of the options list.
51
+ * - DropdownPositions.auto: opens downward by default; flips upward automatically when
52
+ * there is insufficient space below the toggle in the viewport
53
+ * - DropdownPositions.top: always opens upward
54
+ * - DropdownPositions.bottom: always opens downward
55
+ * @default DropdownPositions.auto
56
+ */
57
+ dropdownPosition?: DropdownPositions;
48
58
  }
@@ -87,8 +87,11 @@ export interface PaginationProps extends CommonProps {
87
87
  */
88
88
  isPageFromCountVisible?: boolean;
89
89
  /**
90
- * Props for the rows per page dropdown
91
- * Only used when isRowPerPageVisible is true
90
+ * Props for the rows per page dropdown.
91
+ * Only used when isRowPerPageVisible is true.
92
+ * Use rowPerPageProps.dropdownPosition to control the opening direction
93
+ * ('auto' | 'top' | 'bottom'). Defaults to 'auto', which flips the list
94
+ * upward automatically when there is insufficient space below the toggle.
92
95
  */
93
96
  rowPerPageProps?: RowsPerPageDropdownProps;
94
97
  /**
package/dist/index.js CHANGED
@@ -17033,7 +17033,9 @@ const DropdownContext = /*#__PURE__*/external_react_namespaceObject.createContex
17033
17033
  onChange: () => {
17034
17034
  /* noop */
17035
17035
  },
17036
- maxHeight: 200
17036
+ maxHeight: 200,
17037
+ listRef: undefined,
17038
+ placement: 'bottom'
17037
17039
  });
17038
17040
  function useDropdownContext() {
17039
17041
  return external_react_namespaceObject.useContext(DropdownContext);
@@ -17074,7 +17076,9 @@ function DropdownOptions_EMOTION_STRINGIFIED_CSS_ERROR_() { return "You have tri
17074
17076
 
17075
17077
  const DropdownOptionsBase = /*#__PURE__*/base_default()("ul", true ? {
17076
17078
  target: "e1qg2z4z0"
17077
- } : 0)("position:absolute;width:100%;min-width:max-content;list-style:none;margin:4px 0 0;padding:0;background:", ({
17079
+ } : 0)("position:absolute;width:100%;min-width:max-content;list-style:none;padding:0;", ({
17080
+ placement = 'bottom'
17081
+ }) => placement === 'top' ? 'bottom: 100%; top: auto; margin: 0 0 4px;' : 'top: 100%; bottom: auto; margin: 4px 0 0;', " background:", ({
17078
17082
  theme
17079
17083
  }) => theme.colors.white, ";border-radius:8px;max-height:", ({
17080
17084
  maxHeight = 200
@@ -17102,6 +17106,11 @@ const noItemsMsg = {
17102
17106
  * Renders the scrollable list of options that appears when the dropdown is open.
17103
17107
  * Provides proper ARIA attributes for accessibility and keyboard navigation.
17104
17108
  *
17109
+ * Placement (opening upward or downward) is driven entirely by the parent
17110
+ * Dropdown via context — this component does not calculate position itself.
17111
+ * A ref is attached to the list element so Dropdown can measure its actual
17112
+ * rendered height when determining the correct placement on each open.
17113
+ *
17105
17114
  * @category Form Controls
17106
17115
  * @subcategory Selection
17107
17116
  *
@@ -17135,7 +17144,9 @@ const DropdownOptions = ({
17135
17144
  const {
17136
17145
  onChange,
17137
17146
  activeItem,
17138
- maxHeight
17147
+ maxHeight,
17148
+ listRef,
17149
+ placement
17139
17150
  } = useDropdownContext();
17140
17151
  const childrenArray = external_react_default().Children.toArray(children).filter(Boolean);
17141
17152
 
@@ -17169,11 +17180,13 @@ const DropdownOptions = ({
17169
17180
  }, noItemsMsg.id));
17170
17181
  }
17171
17182
  return (0,jsx_runtime_namespaceObject.jsx)(DropdownOptionsBase, {
17183
+ ref: listRef,
17172
17184
  role: "listbox",
17173
17185
  tabindex: "-1",
17174
17186
  id: id,
17175
17187
  "aria-labelledby": ariaLabelledby,
17176
17188
  maxHeight: maxHeight,
17189
+ placement: placement,
17177
17190
  children: options
17178
17191
  });
17179
17192
  };
@@ -17223,6 +17236,64 @@ const Avatar = /*#__PURE__*/base_default()("div", true ? {
17223
17236
  image
17224
17237
  }) => `url(${image})`, " center/contain no-repeat;" + ( true ? "" : 0));
17225
17238
  /* harmony default export */ const Avatar_Avatar = (Avatar);
17239
+ ;// ./src/components/Dropdown/types.ts
17240
+ let DropdownPositions = /*#__PURE__*/function (DropdownPositions) {
17241
+ DropdownPositions["top"] = "top";
17242
+ DropdownPositions["bottom"] = "bottom";
17243
+ DropdownPositions["auto"] = "auto";
17244
+ return DropdownPositions;
17245
+ }({});
17246
+
17247
+ /**
17248
+ * Props that are controlled by Dropdown component
17249
+ * These props cannot be passed via dropdownProps.toggleButton
17250
+ */
17251
+
17252
+ /**
17253
+ * Props for the Dropdown component
17254
+ *
17255
+ * A select-like dropdown component that allows users to choose one option from
17256
+ * a list. Uses compound component pattern with DropdownOption children.
17257
+ * Provides keyboard navigation, accessibility, customizable styling, and
17258
+ * automatic viewport-aware placement of the options list.
17259
+ *
17260
+ * @example
17261
+ * ```tsx
17262
+ * const items = [
17263
+ * { id: 1, value: 'Option 1' },
17264
+ * { id: 2, value: 'Option 2' },
17265
+ * ];
17266
+ *
17267
+ * <Dropdown
17268
+ * selectedItem={items[0]}
17269
+ * onChange={(item) => console.log(item)}
17270
+ * placeholder="Select an option"
17271
+ * >
17272
+ * {items.map(item => (
17273
+ * <DropdownOption key={item.id} value={item.id}>
17274
+ * {item.value}
17275
+ * </DropdownOption>
17276
+ * ))}
17277
+ * </Dropdown>
17278
+ * ```
17279
+ *
17280
+ * @example
17281
+ * ```tsx
17282
+ * // Force the list to always open upward (e.g. component near the bottom of the page)
17283
+ * <Dropdown
17284
+ * selectedItem={selected}
17285
+ * onChange={handleChange}
17286
+ * dropdownProps={{ dropdownPosition: DropdownPositions.top }}
17287
+ * >
17288
+ * ...
17289
+ * </Dropdown>
17290
+ * ```
17291
+ */
17292
+
17293
+ /**
17294
+ * Dropdown context value
17295
+ * Provides selection state and change handler to child DropdownOption components
17296
+ */
17226
17297
  ;// ./src/components/Dropdown/Dropdown.tsx
17227
17298
 
17228
17299
  function Dropdown_EMOTION_STRINGIFIED_CSS_ERROR_() { return "You have tried to stringify object returned from `css` function. It isn't supposed to be used directly (e.g. as value of the `className` prop), but rather handed to emotion so it can handle it (e.g. as value of `css` prop)."; }
@@ -17235,6 +17306,7 @@ function Dropdown_EMOTION_STRINGIFIED_CSS_ERROR_() { return "You have tried to s
17235
17306
 
17236
17307
 
17237
17308
 
17309
+
17238
17310
  const DropdownBase = /*#__PURE__*/base_default()("div", true ? {
17239
17311
  target: "eizhqtp1"
17240
17312
  } : 0)( true ? {
@@ -17253,8 +17325,13 @@ const SelectedContent = /*#__PURE__*/base_default()("span", true ? {
17253
17325
  *
17254
17326
  * A flexible dropdown component that allows users to select one option from
17255
17327
  * a list of choices. Uses a compound component pattern with DropdownOption
17256
- * children. Provides keyboard navigation, accessibility features, and click-outside
17257
- * to close functionality.
17328
+ * children. Provides keyboard navigation, accessibility features, click-outside
17329
+ * to close functionality, and automatic viewport-aware placement of the options list.
17330
+ *
17331
+ * On every open the component measures available space below the toggle button
17332
+ * and flips the list upward when there is not enough room, preventing the list
17333
+ * from being clipped by the viewport edge. This behavior can be overridden via
17334
+ * dropdownProps.dropdownPosition.
17258
17335
  *
17259
17336
  * Component structure:
17260
17337
  * - Dropdown (root container with context)
@@ -17305,14 +17382,15 @@ const SelectedContent = /*#__PURE__*/base_default()("span", true ? {
17305
17382
  *
17306
17383
  * @example
17307
17384
  * ```tsx
17308
- * // With custom props for sub-components
17385
+ * // With custom props for sub-components and forced upward placement
17309
17386
  * <Dropdown
17310
17387
  * selectedItem={selected}
17311
17388
  * onChange={handleChange}
17312
17389
  * dropdownProps={{
17313
17390
  * base: { id: 'my-dropdown' },
17314
17391
  * toggleButton: { 'data-testid': 'dropdown-toggle' },
17315
- * toggleButtonArrow: { className: 'custom-arrow' }
17392
+ * toggleButtonArrow: { className: 'custom-arrow' },
17393
+ * dropdownPosition: DropdownPositions.top,
17316
17394
  * }}
17317
17395
  * >
17318
17396
  * {options.map(opt => (
@@ -17347,14 +17425,19 @@ const Dropdown = ({
17347
17425
  maxHeight = 200,
17348
17426
  dropdownProps: componentProps
17349
17427
  }) => {
17428
+ const {
17429
+ dropdownPosition = DropdownPositions.auto
17430
+ } = componentProps ?? {};
17350
17431
  const theme = (0,react_namespaceObject.useTheme)();
17351
17432
  const dropdownRef = (0,external_react_namespaceObject.useRef)(null);
17433
+ const listRef = (0,external_react_namespaceObject.useRef)(null);
17352
17434
  const dropdownId = (0,external_react_namespaceObject.useId)();
17353
17435
  const options = [];
17354
17436
  const [isFocused, setIsFocused] = (0,external_react_namespaceObject.useState)(false);
17355
17437
  const [isOpen, setIsOpen] = (0,external_react_namespaceObject.useState)(isInitOpen || false);
17356
17438
  const [colors, setColors] = (0,external_react_namespaceObject.useState)([]);
17357
17439
  const [activeItem, setActiveItem] = (0,external_react_namespaceObject.useState)(selectedItem);
17440
+ const [placement, setPlacement] = (0,external_react_namespaceObject.useState)('bottom');
17358
17441
  const onChange = item => {
17359
17442
  const innerItem = options.filter(option => option.value === item)[0];
17360
17443
  setIsOpen(false);
@@ -17387,6 +17470,17 @@ const Dropdown = ({
17387
17470
  setIsOpen(false);
17388
17471
  }
17389
17472
  }, [isDisabled]);
17473
+ (0,external_react_namespaceObject.useLayoutEffect)(() => {
17474
+ if (!isOpen || !dropdownRef.current) return;
17475
+ if (dropdownPosition !== DropdownPositions.auto) {
17476
+ setPlacement(dropdownPosition);
17477
+ return;
17478
+ }
17479
+ const rect = dropdownRef.current.getBoundingClientRect();
17480
+ const listHeight = listRef.current?.offsetHeight || maxHeight;
17481
+ const spaceBelow = window.innerHeight - rect.bottom;
17482
+ setPlacement(spaceBelow < listHeight ? 'top' : 'bottom');
17483
+ }, [isOpen]);
17390
17484
  const childrenArray = external_react_default().Children.toArray(children).filter(Boolean);
17391
17485
 
17392
17486
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -17401,8 +17495,10 @@ const Dropdown = ({
17401
17495
  const contextValue = external_react_default().useMemo(() => ({
17402
17496
  onChange,
17403
17497
  activeItem,
17404
- maxHeight
17405
- }), [onChange, activeItem, maxHeight]);
17498
+ maxHeight,
17499
+ listRef,
17500
+ placement
17501
+ }), [onChange, activeItem, maxHeight, placement]);
17406
17502
  const value = activeItem ? activeItem.label || activeItem.children || activeItem.value || activeItem || placeholder : placeholder;
17407
17503
  const rawAvatar = activeItem && activeItem.avatar;
17408
17504
  const selectedAvatar = rawAvatar != null ? typeof rawAvatar === 'string' ? (0,jsx_runtime_namespaceObject.jsx)(Avatar_Avatar, {
@@ -49630,6 +49726,7 @@ const RowsPerPageDropdown = ({
49630
49726
  selectedItem = DEFAULT_PER_PAGE_VALUE,
49631
49727
  rowsPerPageList = ROWS_PER_PAGE_LIST,
49632
49728
  rowsPerPageText = 'Rows per page',
49729
+ dropdownPosition,
49633
49730
  ...rest
49634
49731
  }) => {
49635
49732
  const theme = (0,react_namespaceObject.useTheme)();
@@ -49675,7 +49772,8 @@ const RowsPerPageDropdown = ({
49675
49772
  stroke: theme.colors.greyDarker
49676
49773
  }
49677
49774
  }
49678
- }
49775
+ },
49776
+ dropdownPosition
49679
49777
  },
49680
49778
  ...rest,
49681
49779
  children: rowsPerPageList.map(item => (0,jsx_runtime_namespaceObject.jsx)(DropdownOption_DropdownOption, {