@canonical/react-components 1.1.0 → 1.2.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/README.md CHANGED
@@ -41,13 +41,13 @@ You might want to:
41
41
 
42
42
  You may wish to link this library directly to your projects while developing locally.
43
43
 
44
- You can do this by cloning this repo to your local workspace;
44
+ You can do this by cloning this repo to your local workspace:
45
45
 
46
46
  ```shell
47
47
  git clone https://github.com/canonical/react-components
48
48
  ```
49
49
 
50
- If you then drop into that folder and run;
50
+ If you then drop into that folder and run:
51
51
 
52
52
  ```shell
53
53
  yarn run link-packages
@@ -55,7 +55,7 @@ yarn run link-packages
55
55
 
56
56
  ...this will add this project, `react` and `react-dom` to a local yarn registry.
57
57
 
58
- Switching back to the project you are developing, run;
58
+ Switching back to the project you are developing, run:
59
59
 
60
60
  ```shell
61
61
  yarn install
@@ -66,7 +66,7 @@ yarn link @canonical/react-components
66
66
 
67
67
  ...to pull the linked deps from the local registry. If you now run `yarn build-watch` in your `react-components` folder, your project should pick up any changes on refresh or hot module reload.
68
68
 
69
- **Note:** When you're finished working locally - don't forget to go back and unlink;
69
+ **Note:** When you're finished working locally - don't forget to go back and unlink:
70
70
 
71
71
  ```
72
72
  cd react-components
@@ -1,17 +1,13 @@
1
1
  import React from "react";
2
- import type { HTMLProps } from "react";
2
+ import type { HTMLProps, ReactNode } from "react";
3
3
  import type { ButtonProps } from "../Button";
4
4
  import type { ContextualMenuDropdownProps } from "./ContextualMenuDropdown";
5
5
  import type { MenuLink, Position } from "./ContextualMenuDropdown";
6
- import { ClassName, PropsWithSpread, SubComponentProps } from "../../types";
6
+ import { ClassName, ExclusiveProps, PropsWithSpread, SubComponentProps } from "../../types";
7
7
  export declare enum Label {
8
8
  Toggle = "Toggle menu"
9
9
  }
10
- /**
11
- * The props for the ContextualMenu component.
12
- * @template L - The type of the link props.
13
- */
14
- export type Props<L> = PropsWithSpread<{
10
+ export type BaseProps<L> = PropsWithSpread<{
15
11
  /**
16
12
  * Whether the menu should adjust its horizontal position to fit on the screen.
17
13
  */
@@ -44,10 +40,6 @@ export type Props<L> = PropsWithSpread<{
44
40
  * Additional props to pass to the dropdown.
45
41
  */
46
42
  dropdownProps?: SubComponentProps<ContextualMenuDropdownProps>;
47
- /**
48
- * Whether the toggle should display a chevron icon.
49
- */
50
- hasToggleIcon?: boolean;
51
43
  /**
52
44
  * A list of links to display in the menu (if the children prop is not supplied.)
53
45
  */
@@ -68,6 +60,20 @@ export type Props<L> = PropsWithSpread<{
68
60
  * Whether the dropdown should scroll if it is too long to fit on the screen.
69
61
  */
70
62
  scrollOverflow?: boolean;
63
+ /**
64
+ * Whether the menu should be visible.
65
+ */
66
+ visible?: boolean;
67
+ }, HTMLProps<HTMLSpanElement>>;
68
+ /**
69
+ * The props for the ContextualMenu component.
70
+ * @template L - The type of the link props.
71
+ */
72
+ export type Props<L> = BaseProps<L> & ExclusiveProps<{
73
+ /**
74
+ * Whether the toggle should display a chevron icon.
75
+ */
76
+ hasToggleIcon?: boolean;
71
77
  /**
72
78
  * The appearance of the toggle button.
73
79
  */
@@ -92,15 +98,13 @@ export type Props<L> = PropsWithSpread<{
92
98
  * Additional props to pass to the toggle button.
93
99
  */
94
100
  toggleProps?: SubComponentProps<ButtonProps>;
95
- /**
96
- * Whether the menu should be visible.
97
- */
98
- visible?: boolean;
99
- }, HTMLProps<HTMLSpanElement>>;
101
+ }, {
102
+ toggle: ReactNode;
103
+ }>;
100
104
  /**
101
105
  * This is a [React](https://reactjs.org/) component for the Vanilla [Contextual menu](https://docs.vanillaframework.io/patterns//contextual-menu/).
102
106
  *
103
107
  * A contextual menu can be used in conjunction with any page element to provide a contextual menu.
104
108
  */
105
- declare const ContextualMenu: <L>({ autoAdjust, children, className, closeOnEsc, closeOnOutsideClick, constrainPanelWidth, dropdownClassName, dropdownProps, hasToggleIcon, links, onToggleMenu, position, positionNode, scrollOverflow, toggleAppearance, toggleClassName, toggleDisabled, toggleLabel, toggleLabelFirst, toggleProps, visible, ...wrapperProps }: Props<L>) => JSX.Element;
109
+ declare const ContextualMenu: <L>({ autoAdjust, children, className, closeOnEsc, closeOnOutsideClick, constrainPanelWidth, dropdownClassName, dropdownProps, hasToggleIcon, links, onToggleMenu, position, positionNode, scrollOverflow, toggle, toggleAppearance, toggleClassName, toggleDisabled, toggleLabel, toggleLabelFirst, toggleProps, visible, ...wrapperProps }: Props<L>) => JSX.Element;
106
110
  export default ContextualMenu;
@@ -69,6 +69,7 @@ const ContextualMenu = _ref => {
69
69
  position = "right",
70
70
  positionNode,
71
71
  scrollOverflow,
72
+ toggle,
72
73
  toggleAppearance,
73
74
  toggleClassName,
74
75
  toggleDisabled,
@@ -82,7 +83,6 @@ const ContextualMenu = _ref => {
82
83
  const wrapper = (0, _react.useRef)(null);
83
84
  const [positionCoords, setPositionCoords] = (0, _react.useState)();
84
85
  const [adjustedPosition, setAdjustedPosition] = (0, _react.useState)(position);
85
- const hasToggle = hasToggleIcon || toggleLabel;
86
86
  (0, _react.useEffect)(() => {
87
87
  setAdjustedPosition(position);
88
88
  }, [position, autoAdjust]);
@@ -106,14 +106,15 @@ const ContextualMenu = _ref => {
106
106
  isOpen: visible,
107
107
  onOpen: () => {
108
108
  // Call the toggle callback, if supplied.
109
- onToggleMenu && onToggleMenu(true);
109
+ onToggleMenu === null || onToggleMenu === void 0 || onToggleMenu(true);
110
110
  // When the menu opens then update the coordinates of the parent.
111
111
  updatePositionCoords();
112
112
  },
113
113
  onClose: () => {
114
114
  // Call the toggle callback, if supplied.
115
- onToggleMenu && onToggleMenu(false);
116
- }
115
+ onToggleMenu === null || onToggleMenu === void 0 || onToggleMenu(false);
116
+ },
117
+ programmaticallyOpen: true
117
118
  });
118
119
  const previousVisible = (0, _hooks.usePrevious)(visible);
119
120
  const labelNode = toggleLabel && typeof toggleLabel === "string" ? /*#__PURE__*/_react.default.createElement("span", null, toggleLabel) : /*#__PURE__*/_react.default.isValidElement(toggleLabel) ? toggleLabel : null;
@@ -166,32 +167,38 @@ const ContextualMenu = _ref => {
166
167
  }, [positionNode, updatePositionCoords]);
167
168
  (0, _hooks.useListener)(window, onResize, "resize", true, isOpen);
168
169
  (0, _hooks.useListener)(window, onScroll, "scroll", false, isOpen, true);
170
+ let toggleNode = null;
171
+ if (toggle) {
172
+ toggleNode = toggle;
173
+ } else if (hasToggleIcon || toggleLabel) {
174
+ toggleNode = /*#__PURE__*/_react.default.createElement(_Button.default, _extends({
175
+ appearance: toggleAppearance,
176
+ "aria-controls": id,
177
+ "aria-expanded": isOpen ? "true" : "false",
178
+ "aria-label": toggleLabel ? null : Label.Toggle,
179
+ "aria-pressed": isOpen ? "true" : "false",
180
+ "aria-haspopup": "true",
181
+ className: (0, _classnames.default)("p-contextual-menu__toggle", toggleClassName),
182
+ disabled: toggleDisabled,
183
+ hasIcon: hasToggleIcon,
184
+ onClick: evt => {
185
+ if (!isOpen) {
186
+ openPortal(evt);
187
+ } else {
188
+ closePortal(evt);
189
+ }
190
+ },
191
+ type: "button"
192
+ }, toggleProps), toggleLabelFirst ? labelNode : null, hasToggleIcon ? /*#__PURE__*/_react.default.createElement("i", {
193
+ className: (0, _classnames.default)("p-icon--chevron-down p-contextual-menu__indicator", {
194
+ "is-light": ["negative", "positive"].includes(toggleAppearance)
195
+ })
196
+ }) : null, toggleLabelFirst ? null : labelNode);
197
+ }
169
198
  return /*#__PURE__*/_react.default.createElement("span", _extends({
170
199
  className: contextualMenuClassName,
171
200
  ref: wrapperRef
172
- }, wrapperProps), hasToggle ? /*#__PURE__*/_react.default.createElement(_Button.default, _extends({
173
- appearance: toggleAppearance,
174
- "aria-controls": id,
175
- "aria-expanded": isOpen ? "true" : "false",
176
- "aria-label": toggleLabel ? null : Label.Toggle,
177
- "aria-pressed": isOpen ? "true" : "false",
178
- "aria-haspopup": "true",
179
- className: (0, _classnames.default)("p-contextual-menu__toggle", toggleClassName),
180
- disabled: toggleDisabled,
181
- hasIcon: hasToggleIcon,
182
- onClick: evt => {
183
- if (!isOpen) {
184
- openPortal(evt);
185
- } else {
186
- closePortal(evt);
187
- }
188
- },
189
- type: "button"
190
- }, toggleProps), toggleLabelFirst ? labelNode : null, hasToggleIcon ? /*#__PURE__*/_react.default.createElement("i", {
191
- className: (0, _classnames.default)("p-icon--chevron-down p-contextual-menu__indicator", {
192
- "is-light": ["negative", "positive"].includes(toggleAppearance)
193
- })
194
- }) : null, toggleLabelFirst ? null : labelNode) : null, isOpen && /*#__PURE__*/_react.default.createElement(Portal, null, /*#__PURE__*/_react.default.createElement(_ContextualMenuDropdown.default, _extends({
201
+ }, wrapperProps), toggleNode, isOpen && /*#__PURE__*/_react.default.createElement(Portal, null, /*#__PURE__*/_react.default.createElement(_ContextualMenuDropdown.default, _extends({
195
202
  adjustedPosition: adjustedPosition,
196
203
  autoAdjust: autoAdjust,
197
204
  handleClose: closePortal,
@@ -40,7 +40,6 @@ type MultiSelectDropdownProps = {
40
40
  footer?: ReactNode;
41
41
  groupFn?: GroupFn;
42
42
  sortFn?: SortFn;
43
- shouldPinSelectedItems?: boolean;
44
43
  } & React.HTMLAttributes<HTMLDivElement>;
45
44
  declare const sortAlphabetically: (a: MultiSelectItem, b: MultiSelectItem) => number;
46
45
  declare const getGroupedItems: (items: MultiSelectItem[]) => {
@@ -155,8 +155,7 @@ MultiSelectDropdown.propTypes = {
155
155
  onSelectItem: _propTypes.default.func,
156
156
  footer: _propTypes.default.node,
157
157
  groupFn: _propTypes.default.func,
158
- sortFn: _propTypes.default.any,
159
- shouldPinSelectedItems: _propTypes.default.bool
158
+ sortFn: _propTypes.default.any
160
159
  };
161
160
  const MultiSelect = _ref4 => {
162
161
  let {
@@ -176,21 +175,9 @@ const MultiSelect = _ref4 => {
176
175
  showDropdownFooter = true,
177
176
  variant = "search"
178
177
  } = _ref4;
179
- const wrapperRef = (0, _index.useClickOutside)(() => {
180
- setIsDropdownOpen(false);
181
- setFilter("");
182
- });
183
- (0, _index.useOnEscapePressed)(() => {
184
- setIsDropdownOpen(false);
185
- setFilter("");
186
- });
178
+ const buttonRef = (0, _react.useRef)();
187
179
  const [isDropdownOpen, setIsDropdownOpen] = (0, _react.useState)(false);
188
180
  const [filter, setFilter] = (0, _react.useState)("");
189
- (0, _react.useEffect)(() => {
190
- if (!isDropdownOpen) {
191
- setFilter("");
192
- }
193
- }, [isDropdownOpen]);
194
181
  const [internalSelectedItems, setInternalSelectedItems] = (0, _react.useState)([]);
195
182
  const selectedItems = externalSelectedItems || internalSelectedItems;
196
183
  const updateItems = newItems => {
@@ -219,43 +206,69 @@ const MultiSelect = _ref4 => {
219
206
  type: "button"
220
207
  }, "Clear"));
221
208
  }
222
- return /*#__PURE__*/_react.default.createElement("div", {
223
- ref: wrapperRef
224
- }, /*#__PURE__*/_react.default.createElement("div", {
225
- className: "multi-select"
226
- }, variant === "search" ? /*#__PURE__*/_react.default.createElement(_index.SearchBox, {
227
- externallyControlled: true,
228
- "aria-controls": dropdownId,
229
- "aria-expanded": isDropdownOpen,
230
- id: inputId,
231
- role: "combobox",
232
- "aria-label": label || placeholder || "Search",
233
- disabled: disabled,
234
- autoComplete: "off",
235
- onChange: value => {
236
- setFilter(value);
237
- // reopen if dropdown has been closed via ESC
238
- setIsDropdownOpen(true);
209
+ return /*#__PURE__*/_react.default.createElement(_index.ContextualMenu, {
210
+ className: "multi-select",
211
+ onToggleMenu: isOpen => {
212
+ if (!isOpen) {
213
+ setFilter("");
214
+ }
215
+ // Handle syncing the state when toggling the menu from within the
216
+ // contextual menu component e.g. when clicking outside.
217
+ if (isOpen !== isDropdownOpen) {
218
+ setIsDropdownOpen(isOpen);
219
+ }
239
220
  },
240
- onFocus: () => setIsDropdownOpen(true),
241
- placeholder: placeholder !== null && placeholder !== void 0 ? placeholder : "Search",
242
- required: required,
243
- type: "text",
244
- value: filter,
245
- className: "multi-select__input"
246
- }) : /*#__PURE__*/_react.default.createElement("button", {
247
- role: "combobox",
248
- type: "button",
249
- "aria-label": label || placeholder || "Select items",
250
- "aria-controls": dropdownId,
251
- "aria-expanded": isDropdownOpen,
252
- className: "multi-select__select-button",
253
- onClick: () => {
254
- setIsDropdownOpen(isOpen => !isOpen);
255
- }
256
- }, /*#__PURE__*/_react.default.createElement("span", {
257
- className: "multi-select__condensed-text"
258
- }, listSelected && selectedItems.length > 0 ? selectedItemsLabel : placeholder !== null && placeholder !== void 0 ? placeholder : "Select items")), /*#__PURE__*/_react.default.createElement(MultiSelectDropdown, {
221
+ position: "left",
222
+ constrainPanelWidth: true,
223
+ toggle: variant === "search" ? /*#__PURE__*/_react.default.createElement(_index.SearchBox, {
224
+ externallyControlled: true,
225
+ "aria-controls": dropdownId,
226
+ "aria-expanded": isDropdownOpen,
227
+ id: inputId,
228
+ role: "combobox",
229
+ "aria-label": label || placeholder || "Search",
230
+ disabled: disabled,
231
+ autoComplete: "off",
232
+ onChange: value => {
233
+ setFilter(value);
234
+ // reopen if dropdown has been closed via ESC
235
+ setIsDropdownOpen(true);
236
+ },
237
+ onFocus: () => setIsDropdownOpen(true),
238
+ placeholder: placeholder !== null && placeholder !== void 0 ? placeholder : "Search",
239
+ required: required,
240
+ type: "text",
241
+ value: filter,
242
+ className: "multi-select__input"
243
+ }) : /*#__PURE__*/_react.default.createElement("button", {
244
+ role: "combobox",
245
+ type: "button",
246
+ "aria-label": label || placeholder || "Select items",
247
+ "aria-controls": dropdownId,
248
+ "aria-expanded": isDropdownOpen,
249
+ className: "multi-select__select-button",
250
+ onClick: () => {
251
+ setIsDropdownOpen(!isDropdownOpen);
252
+ },
253
+ onMouseDown: event => {
254
+ // If the dropdown is open when this button is clicked the
255
+ // click-outside event will fire which will close the dropdown, but
256
+ // then the button click event will fire which will immediately
257
+ // reopen the dropdown.
258
+ // To prevent this we can stop the propagation to the click event
259
+ // while `isDropdownOpen` is still set to `true` (by the time we
260
+ // get to the `onClick` event `isDropdownOpen` will already be `false`,
261
+ // hence having to do this on mouse down).
262
+ if (isDropdownOpen) {
263
+ event.stopPropagation();
264
+ }
265
+ },
266
+ ref: buttonRef
267
+ }, /*#__PURE__*/_react.default.createElement("span", {
268
+ className: "multi-select__condensed-text"
269
+ }, listSelected && selectedItems.length > 0 ? selectedItemsLabel : placeholder !== null && placeholder !== void 0 ? placeholder : "Select items")),
270
+ visible: isDropdownOpen
271
+ }, /*#__PURE__*/_react.default.createElement(MultiSelectDropdown, {
259
272
  id: dropdownId,
260
273
  isOpen: isDropdownOpen,
261
274
  items: filter.length > 0 ? items.filter(item => item.label.toLowerCase().includes(filter.toLowerCase())) : items,
@@ -266,7 +279,7 @@ const MultiSelect = _ref4 => {
266
279
  onSelectItem: onSelectItem,
267
280
  onDeselectItem: onDeselectItem,
268
281
  footer: footer
269
- })));
282
+ }));
270
283
  };
271
284
  exports.MultiSelect = MultiSelect;
272
285
  MultiSelect.propTypes = {
@@ -10,7 +10,9 @@ $dropdown-max-height: 20rem;
10
10
  // XXX: This is a workaround for https://github.com/canonical/vanilla-framework/issues/5030
11
11
  @include vf-b-forms;
12
12
 
13
+ margin-bottom: $input-margin-bottom;
13
14
  position: relative;
15
+ width: 100%;
14
16
  }
15
17
 
16
18
  .multi-select .p-form-validation__message {
@@ -26,6 +28,7 @@ $dropdown-max-height: 20rem;
26
28
 
27
29
  .multi-select__input {
28
30
  cursor: pointer;
31
+ margin-bottom: 0;
29
32
  position: relative;
30
33
 
31
34
  &.items-selected {
@@ -40,19 +43,6 @@ $dropdown-max-height: 20rem;
40
43
  }
41
44
  }
42
45
 
43
- .multi-select__dropdown {
44
- background-color: $colors--theme--background-default;
45
- box-shadow: $box-shadow;
46
- color: $colors--theme--text-default;
47
- left: 0;
48
- max-height: $dropdown-max-height;
49
- overflow: auto;
50
- padding-top: $spv--small;
51
- position: absolute;
52
- right: 0;
53
- top: calc(100% - #{$input-margin-bottom});
54
- }
55
-
56
46
  .multi-select__dropdown--side-by-side {
57
47
  display: flex;
58
48
  flex-wrap: wrap;
@@ -132,6 +122,10 @@ $dropdown-max-height: 20rem;
132
122
  position: relative;
133
123
  z-index: 0;
134
124
 
125
+ .multi-select & {
126
+ margin-bottom: 0;
127
+ }
128
+
135
129
  &::after {
136
130
  content: "";
137
131
  margin-left: $sph--large;
@@ -28,7 +28,7 @@ export type Props = PropsWithSpread<{
28
28
  /**
29
29
  * A function that is called when the user clicks the search icon or presses enter
30
30
  */
31
- onSearch?: () => void;
31
+ onSearch?: (inputValue: string) => void;
32
32
  /**
33
33
  * A function that is called when the user clicks the reset icon
34
34
  */
@@ -37,6 +37,10 @@ export type Props = PropsWithSpread<{
37
37
  * A search input placeholder message.
38
38
  */
39
39
  placeholder?: string;
40
+ /**
41
+ * Whether the search input should lose focus when searching.
42
+ */
43
+ shouldBlurOnSearch?: boolean;
40
44
  /**
41
45
  * Whether the search input should receive focus after pressing the reset button
42
46
  */
@@ -31,6 +31,7 @@ const SearchBox = /*#__PURE__*/_react.default.forwardRef((_ref, forwardedRef) =>
31
31
  onSearch,
32
32
  onClear,
33
33
  placeholder = "Search",
34
+ shouldBlurOnSearch = true,
34
35
  shouldRefocusAfterReset,
35
36
  value,
36
37
  ...props
@@ -45,11 +46,13 @@ const SearchBox = /*#__PURE__*/_react.default.forwardRef((_ref, forwardedRef) =>
45
46
  }
46
47
  };
47
48
  const triggerSearch = () => {
48
- onSearch && onSearch();
49
+ onSearch === null || onSearch === void 0 || onSearch(externallyControlled ? value : internalRef.current.value);
49
50
  };
50
51
  const onKeyDown = e => {
51
52
  if (e.key === "Enter" && internalRef.current.checkValidity()) {
52
- internalRef.current.blur();
53
+ if (shouldBlurOnSearch) {
54
+ internalRef.current.blur();
55
+ }
53
56
  triggerSearch();
54
57
  }
55
58
  };
@@ -9,5 +9,5 @@ export type Props<L = LinkDefaultElement> = Omit<SideNavigationBaseProps<L>, "co
9
9
  */
10
10
  component?: SideNavigationBaseProps<L>["component"];
11
11
  };
12
- declare const SideNavigationLink: <L = LinkDefaultElement>({ component, ...props }: Props<L>) => React.JSX.Element;
12
+ declare const SideNavigationLink: React.ForwardRefExoticComponent<Omit<Props<LinkDefaultElement>, "ref"> & React.RefAttributes<HTMLAnchorElement>>;
13
13
  export default SideNavigationLink;
@@ -4,14 +4,17 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = void 0;
7
- var _react = _interopRequireDefault(require("react"));
7
+ var _react = _interopRequireWildcard(require("react"));
8
8
  var _classnames = _interopRequireDefault(require("classnames"));
9
9
  var _SideNavigationBase = _interopRequireDefault(require("../SideNavigationBase"));
10
10
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
11
+ function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
12
+ function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
11
13
  function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
12
- const SideNavigationLink = _ref => {
14
+ const SideNavigationLink = /*#__PURE__*/(0, _react.forwardRef)(_ref => {
13
15
  let {
14
16
  component,
17
+ ref,
15
18
  ...props
16
19
  } = _ref;
17
20
  let className = null;
@@ -23,5 +26,5 @@ const SideNavigationLink = _ref => {
23
26
  className: (0, _classnames.default)("p-side-navigation__link", className),
24
27
  component: component !== null && component !== void 0 ? component : "a"
25
28
  }, props));
26
- };
29
+ });
27
30
  var _default = exports.default = SideNavigationLink;
@@ -4,7 +4,7 @@ export type BasePaginationProps = {
4
4
  /**
5
5
  * list of data elements to be paginated. This component is un-opinionated about
6
6
  * the structure of the data but it should be identical to the data structure
7
- * reuiqred by the child table component
7
+ * required by the child table component
8
8
  */
9
9
  data: unknown[];
10
10
  /**
@@ -94,7 +94,7 @@ const TablePagination = props => {
94
94
  };
95
95
  const clonedChildren = (0, _utils.renderChildren)(children, dataForwardProp, controlData);
96
96
  const controls = /*#__PURE__*/_react.default.createElement(_TablePaginationControls.default, _extends({}, divProps, {
97
- data: controlData,
97
+ visibleCount: controlData.length,
98
98
  className: className,
99
99
  itemName: itemName,
100
100
  description: description,
@@ -18,7 +18,7 @@
18
18
  }
19
19
 
20
20
  .back {
21
- margin: 0 $spv--large;
21
+ margin: 0 0 0 $spv--large;
22
22
 
23
23
  .p-icon--chevron-down {
24
24
  rotate: 90deg;
@@ -34,7 +34,7 @@
34
34
  }
35
35
 
36
36
  .pagination-input {
37
- margin-right: $spv--small;
37
+ margin: 0 $spv--small 0 $spv--large;
38
38
  min-width: 0;
39
39
  width: 3rem;
40
40
  }
@@ -8,3 +8,7 @@ export declare const CustomPageLimit: Story;
8
8
  export declare const CustomDisplayTitle: Story;
9
9
  export declare const RenderAbove: Story;
10
10
  export declare const RenderBelow: Story;
11
+ /** The table pagination controls can be used without wrapping MainTable by
12
+ * using the `TablePaginationControls` component.
13
+ */
14
+ export declare const ControlsOnly: Story;
@@ -3,10 +3,11 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.default = exports.RenderBelow = exports.RenderAbove = exports.Default = exports.CustomPageLimit = exports.CustomDisplayTitle = void 0;
6
+ exports.default = exports.RenderBelow = exports.RenderAbove = exports.Default = exports.CustomPageLimit = exports.CustomDisplayTitle = exports.ControlsOnly = void 0;
7
7
  var _react = _interopRequireDefault(require("react"));
8
8
  var _TablePagination = _interopRequireDefault(require("./TablePagination"));
9
9
  var _MainTable = _interopRequireDefault(require("../MainTable"));
10
+ var _TablePaginationControls = _interopRequireDefault(require("./TablePaginationControls"));
10
11
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
11
12
  const meta = {
12
13
  component: _TablePagination.default,
@@ -299,4 +300,31 @@ const RenderBelow = exports.RenderBelow = {
299
300
  }));
300
301
  },
301
302
  name: "RenderBelow"
303
+ };
304
+
305
+ /** The table pagination controls can be used without wrapping MainTable by
306
+ * using the `TablePaginationControls` component.
307
+ */
308
+ const ControlsOnly = exports.ControlsOnly = {
309
+ render: () => {
310
+ return /*#__PURE__*/_react.default.createElement(_TablePaginationControls.default, {
311
+ currentPage: 1,
312
+ itemName: "row",
313
+ nextButtonProps: {
314
+ disabled: false
315
+ },
316
+ onInputPageChange: console.log,
317
+ onNextPage: console.log,
318
+ onPageSizeChange: console.log,
319
+ onPreviousPage: console.log,
320
+ pageLimits: [10, 25, 50],
321
+ pageSize: 20,
322
+ previousButtonProps: {
323
+ disabled: false
324
+ },
325
+ showPageInput: true,
326
+ totalItems: 100,
327
+ visibleCount: 10
328
+ });
329
+ }
302
330
  };
@@ -1,6 +1,24 @@
1
+ import { ButtonProps } from "../../Button";
1
2
  import { HTMLAttributes } from "react";
2
3
  import { BasePaginationProps, ExternalControlProps, InternalControlProps } from "../TablePagination";
4
+ export declare enum Label {
5
+ NEXT_PAGE = "Next page",
6
+ PREVIOUS_PAGE = "Previous page",
7
+ PAGE_NUMBER = "Page number"
8
+ }
3
9
  export type AllProps = BasePaginationProps & InternalControlProps & ExternalControlProps;
4
- export type Props = Omit<AllProps, "externallyControlled" | "dataForwardProp" | "position"> & HTMLAttributes<HTMLDivElement>;
5
- declare const TablePaginationControls: ({ data, className, itemName, description, pageLimits, totalItems, currentPage, pageSize, onPageChange, onPageSizeChange, ...divProps }: Props) => JSX.Element;
10
+ export type Props = Omit<AllProps, "currentPage" | "data" | "dataForwardProp" | "externallyControlled" | "onPageChange" | "position" | "totalItems"> & {
11
+ currentPage?: AllProps["currentPage"];
12
+ displayDescription?: boolean;
13
+ onInputPageChange?: (page: number) => void;
14
+ nextButtonProps?: Partial<ButtonProps>;
15
+ onNextPage?: (page: number) => void;
16
+ onPageChange?: AllProps["onPageChange"];
17
+ onPreviousPage?: (page: number) => void;
18
+ previousButtonProps?: Partial<ButtonProps>;
19
+ totalItems?: AllProps["totalItems"];
20
+ visibleCount?: number;
21
+ showPageInput?: boolean;
22
+ } & HTMLAttributes<HTMLDivElement>;
23
+ declare const TablePaginationControls: ({ className, currentPage, description, displayDescription, onInputPageChange, itemName, nextButtonProps, onNextPage, onPageChange, onPageSizeChange, onPreviousPage, pageLimits, pageSize, previousButtonProps, showPageInput, totalItems, visibleCount, ...divProps }: Props) => JSX.Element;
6
24
  export default TablePaginationControls;
@@ -3,7 +3,8 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.default = void 0;
6
+ exports.default = exports.Label = void 0;
7
+ var _propTypes = _interopRequireDefault(require("prop-types"));
7
8
  var _Button = _interopRequireDefault(require("../../Button"));
8
9
  var _Icon = _interopRequireDefault(require("../../Icon"));
9
10
  var _Input = _interopRequireDefault(require("../../Input"));
@@ -13,42 +14,58 @@ var _classnames = _interopRequireDefault(require("classnames"));
13
14
  var _utils = require("../utils");
14
15
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
15
16
  function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
17
+ let Label = exports.Label = /*#__PURE__*/function (Label) {
18
+ Label["NEXT_PAGE"] = "Next page";
19
+ Label["PREVIOUS_PAGE"] = "Previous page";
20
+ Label["PAGE_NUMBER"] = "Page number";
21
+ return Label;
22
+ }({});
16
23
  const TablePaginationControls = _ref => {
17
24
  let {
18
- data,
19
25
  className,
20
- itemName,
21
- description,
22
- pageLimits,
23
- totalItems,
24
26
  currentPage,
25
- pageSize,
27
+ description,
28
+ displayDescription = true,
29
+ onInputPageChange,
30
+ itemName,
31
+ nextButtonProps,
32
+ onNextPage,
26
33
  onPageChange,
27
34
  onPageSizeChange,
35
+ onPreviousPage,
36
+ pageLimits,
37
+ pageSize,
38
+ previousButtonProps,
39
+ showPageInput = true,
40
+ totalItems,
41
+ visibleCount,
28
42
  ...divProps
29
43
  } = _ref;
30
44
  const isSmallScreen = (0, _utils.useFigureSmallScreen)();
31
- const totalPages = Math.ceil(totalItems / pageSize);
45
+ const totalPages = totalItems ? Math.ceil(totalItems / pageSize) : null;
32
46
  const descriptionDisplay = (0, _utils.getDescription)({
33
47
  description,
34
- data,
48
+ visibleCount,
35
49
  isSmallScreen,
36
50
  totalItems,
37
51
  itemName
38
52
  });
39
53
  const handleDecrementPage = currentPage => {
40
54
  if (currentPage > 1) {
41
- onPageChange(currentPage - 1);
55
+ onPageChange === null || onPageChange === void 0 || onPageChange(currentPage - 1);
42
56
  }
57
+ onPreviousPage === null || onPreviousPage === void 0 || onPreviousPage(typeof currentPage === "number" ? currentPage - 1 : null);
43
58
  };
44
59
  const handleIncrementPage = (currentPage, totalPages) => {
45
60
  if (currentPage < totalPages) {
46
- onPageChange(currentPage + 1);
61
+ onPageChange === null || onPageChange === void 0 || onPageChange(currentPage + 1);
47
62
  }
63
+ onNextPage === null || onNextPage === void 0 || onNextPage(typeof currentPage === "number" ? currentPage + 1 : null);
48
64
  };
49
65
  const handleInputPageChange = e => {
50
66
  const newPage = Math.min(totalPages, Math.max(1, parseInt(e.target.value)));
51
- onPageChange(newPage);
67
+ onPageChange === null || onPageChange === void 0 || onPageChange(newPage);
68
+ onInputPageChange === null || onInputPageChange === void 0 || onInputPageChange(Number(e.target.value));
52
69
  };
53
70
  const handlePageSizeChange = e => {
54
71
  onPageSizeChange(parseInt(e.target.value));
@@ -60,31 +77,31 @@ const TablePaginationControls = _ref => {
60
77
  }), /*#__PURE__*/_react.default.createElement("div", {
61
78
  className: "description",
62
79
  id: "pagination-description"
63
- }, descriptionDisplay), /*#__PURE__*/_react.default.createElement(_Button.default, {
64
- "aria-label": "Previous page",
80
+ }, displayDescription ? descriptionDisplay : null), /*#__PURE__*/_react.default.createElement(_Button.default, _extends({
81
+ "aria-label": Label.PREVIOUS_PAGE,
65
82
  className: "back",
66
83
  appearance: "base",
67
84
  hasIcon: true,
68
85
  disabled: currentPage === 1,
69
86
  onClick: () => handleDecrementPage(currentPage)
70
- }, /*#__PURE__*/_react.default.createElement(_Icon.default, {
87
+ }, previousButtonProps), previousButtonProps !== null && previousButtonProps !== void 0 && previousButtonProps.children ? previousButtonProps.children : /*#__PURE__*/_react.default.createElement(_Icon.default, {
71
88
  name: "chevron-down"
72
- })), /*#__PURE__*/_react.default.createElement(_Input.default, {
89
+ })), showPageInput ? /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_Input.default, {
73
90
  id: "paginationPageInput",
74
- label: "Page number",
91
+ label: Label.PAGE_NUMBER,
75
92
  labelClassName: "u-off-screen",
76
93
  className: "u-no-margin--bottom pagination-input",
77
94
  onChange: handleInputPageChange,
78
95
  value: currentPage,
79
96
  type: "number"
80
- }), " ", "of\xA0", totalPages, /*#__PURE__*/_react.default.createElement(_Button.default, {
81
- "aria-label": "Next page",
97
+ }), " ") : null, typeof totalPages === "number" ? "of ".concat(totalPages) : null, /*#__PURE__*/_react.default.createElement(_Button.default, _extends({
98
+ "aria-label": Label.NEXT_PAGE,
82
99
  className: "next",
83
100
  appearance: "base",
84
101
  hasIcon: true,
85
102
  disabled: currentPage === totalPages,
86
103
  onClick: () => handleIncrementPage(currentPage, totalPages)
87
- }, /*#__PURE__*/_react.default.createElement(_Icon.default, {
104
+ }, nextButtonProps), /*#__PURE__*/_react.default.createElement(_Icon.default, {
88
105
  name: "chevron-down"
89
106
  })), /*#__PURE__*/_react.default.createElement(_Select.default, {
90
107
  className: "u-no-margin--bottom",
@@ -96,4 +113,12 @@ const TablePaginationControls = _ref => {
96
113
  value: pageSize
97
114
  }));
98
115
  };
116
+ TablePaginationControls.propTypes = {
117
+ displayDescription: _propTypes.default.bool,
118
+ onInputPageChange: _propTypes.default.func,
119
+ onNextPage: _propTypes.default.func,
120
+ onPreviousPage: _propTypes.default.func,
121
+ visibleCount: _propTypes.default.number,
122
+ showPageInput: _propTypes.default.bool
123
+ };
99
124
  var _default = exports.default = TablePaginationControls;
@@ -17,11 +17,11 @@ export declare const generatePagingOptions: (pageLimits: number[]) => {
17
17
  value: number;
18
18
  label: string;
19
19
  }[];
20
- export declare const getDescription: ({ description, data, isSmallScreen, totalItems, itemName, }: {
20
+ export declare const getDescription: ({ description, isSmallScreen, totalItems, itemName, visibleCount, }: {
21
21
  description: ReactNode;
22
- data: unknown[];
23
22
  isSmallScreen: boolean;
24
23
  totalItems: number;
25
24
  itemName: string;
25
+ visibleCount: number;
26
26
  }) => string | number | true | ReactElement<any, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode>;
27
27
  export declare const useFigureSmallScreen: () => boolean;
@@ -44,15 +44,14 @@ exports.generatePagingOptions = generatePagingOptions;
44
44
  const getDescription = _ref => {
45
45
  let {
46
46
  description,
47
- data,
48
47
  isSmallScreen,
49
48
  totalItems,
50
- itemName
49
+ itemName,
50
+ visibleCount
51
51
  } = _ref;
52
52
  if (description) {
53
53
  return description;
54
54
  }
55
- const visibleCount = data.length;
56
55
  if (isSmallScreen) {
57
56
  return "".concat(visibleCount, " out of ").concat(totalItems);
58
57
  }
package/dist/index.d.ts CHANGED
@@ -63,6 +63,7 @@ export { default as Tabs } from "./components/Tabs";
63
63
  export { default as Textarea } from "./components/Textarea";
64
64
  export { default as Tooltip } from "./components/Tooltip";
65
65
  export { default as TablePagination } from "./components/TablePagination";
66
+ export { default as TablePaginationControls } from "./components/TablePagination/TablePaginationControls";
66
67
  export type { AccordionProps } from "./components/Accordion";
67
68
  export type { ActionButtonProps } from "./components/ActionButton";
68
69
  export type { ArticlePaginationProps } from "./components/ArticlePagination";
package/dist/index.js CHANGED
@@ -79,6 +79,7 @@ var _exportNames = {
79
79
  Textarea: true,
80
80
  Tooltip: true,
81
81
  TablePagination: true,
82
+ TablePaginationControls: true,
82
83
  useOnClickOutside: true,
83
84
  useClickOutside: true,
84
85
  useId: true,
@@ -488,6 +489,12 @@ Object.defineProperty(exports, "TablePagination", {
488
489
  return _TablePagination.default;
489
490
  }
490
491
  });
492
+ Object.defineProperty(exports, "TablePaginationControls", {
493
+ enumerable: true,
494
+ get: function () {
495
+ return _TablePaginationControls.default;
496
+ }
497
+ });
491
498
  Object.defineProperty(exports, "TableRow", {
492
499
  enumerable: true,
493
500
  get: function () {
@@ -690,6 +697,7 @@ var _Tabs = _interopRequireDefault(require("./components/Tabs"));
690
697
  var _Textarea = _interopRequireDefault(require("./components/Textarea"));
691
698
  var _Tooltip = _interopRequireDefault(require("./components/Tooltip"));
692
699
  var _TablePagination = _interopRequireDefault(require("./components/TablePagination"));
700
+ var _TablePaginationControls = _interopRequireDefault(require("./components/TablePagination/TablePaginationControls"));
693
701
  var _hooks = require("./hooks");
694
702
  var _utils = require("./utils");
695
703
  var _enums = require("./enums");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonical/react-components",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "author": {
@@ -89,9 +89,9 @@
89
89
  "ts-jest": "29.1.2",
90
90
  "tsc-alias": "1.8.8",
91
91
  "typescript": "5.4.5",
92
- "vanilla-framework": "4.11.0",
92
+ "vanilla-framework": "4.15.0",
93
93
  "wait-on": "7.2.0",
94
- "webpack": "5.91.0"
94
+ "webpack": "5.94.0"
95
95
  },
96
96
  "dependencies": {
97
97
  "@types/jest": "29.5.12",
@@ -113,11 +113,11 @@
113
113
  "jackspeak": "2.1.1"
114
114
  },
115
115
  "peerDependencies": {
116
- "@types/react": "^17.0.2 || ^18.0.0",
117
- "@types/react-dom": "^17.0.2 || ^18.0.0",
116
+ "@types/react": "^18.0.0",
117
+ "@types/react-dom": "^18.0.0",
118
118
  "formik": "^2.4.5",
119
- "react": "^17.0.2 || ^18.0.0",
120
- "react-dom": "^17.0.2 || ^18.0.0",
119
+ "react": "^18.0.0",
120
+ "react-dom": "^18.0.0",
121
121
  "vanilla-framework": "^3.15.1 || ^4.0.0"
122
122
  },
123
123
  "scripts": {