playbook_ui 16.11.0.pre.rc.1 → 16.11.0.pre.rc.2

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_advanced_table/Components/RegularTableView.tsx +19 -6
  3. data/app/pb_kits/playbook/pb_advanced_table/Hooks/useTableState.ts +17 -12
  4. data/app/pb_kits/playbook/pb_advanced_table/_advanced_table.scss +3 -0
  5. data/app/pb_kits/playbook/pb_advanced_table/advanced_table.test.jsx +26 -1
  6. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pinned_rows_both.jsx +57 -0
  7. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pinned_rows_both_react.md +1 -0
  8. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pinned_rows_bottom.jsx +57 -0
  9. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pinned_rows_bottom_react.md +1 -0
  10. data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_pinned_rows_react.md +2 -2
  11. data/app/pb_kits/playbook/pb_advanced_table/docs/example.yml +3 -2
  12. data/app/pb_kits/playbook/pb_advanced_table/docs/index.js +3 -1
  13. data/app/pb_kits/playbook/pb_dialog/_dialog.tsx +18 -3
  14. data/app/pb_kits/playbook/pb_file_upload/docs/_file_upload_remove_replace.html.erb +66 -0
  15. data/app/pb_kits/playbook/pb_file_upload/docs/_file_upload_remove_replace.jsx +53 -0
  16. data/app/pb_kits/playbook/pb_file_upload/docs/_file_upload_remove_replace.md +5 -0
  17. data/app/pb_kits/playbook/pb_file_upload/docs/example.yml +3 -1
  18. data/app/pb_kits/playbook/pb_file_upload/docs/index.js +1 -0
  19. data/app/pb_kits/playbook/pb_full_screen/_full_screen.scss +15 -0
  20. data/app/pb_kits/playbook/pb_full_screen/_full_screen.tsx +21 -6
  21. data/app/pb_kits/playbook/pb_full_screen/context/_full_screen_context.tsx +9 -0
  22. data/app/pb_kits/playbook/pb_full_screen/docs/_full_screen_table_and_filter.jsx +1 -1
  23. data/app/pb_kits/playbook/pb_full_screen/docs/_full_screen_table_and_filter.md +1 -1
  24. data/app/pb_kits/playbook/pb_full_screen/full_screen.test.jsx +17 -0
  25. data/app/pb_kits/playbook/pb_popover/_popover.tsx +13 -8
  26. data/app/pb_kits/playbook/pb_popover/popover.test.js +36 -0
  27. data/app/pb_kits/playbook/pb_textarea/docs/_textarea_error.html.erb +1 -1
  28. data/app/pb_kits/playbook/utilities/floatingPortalHosts.ts +38 -3
  29. data/dist/chunks/{_pb_line_graph-C2H9vs_8.js → _pb_line_graph-Gn_9RYbf.js} +1 -1
  30. data/dist/chunks/{_typeahead-Du8dRYaM.js → _typeahead-COksV0jV.js} +1 -1
  31. data/dist/chunks/{globalProps-DIAZ03-L.js → globalProps-CVEUyQe6.js} +1 -1
  32. data/dist/chunks/{lib-ZHDedNfP.js → lib-CYrXqTdQ.js} +2 -2
  33. data/dist/chunks/vendor.js +4 -4
  34. data/dist/menu.yml +1 -1
  35. data/dist/playbook-rails-react-bindings.js +1 -1
  36. data/dist/playbook-rails.js +1 -1
  37. data/dist/playbook.css +1 -1
  38. data/lib/playbook/version.rb +1 -1
  39. metadata +14 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 848af7a36c085122e6e8156e310bd4f6194e308bbb74cd1b3662b14ec15b422f
4
- data.tar.gz: e48ac7afb5ae31df9aa428eda784862d970b793b9fb9f675102d93a358c147f9
3
+ metadata.gz: 3240b5088da697dc630863038b107d3afb359c41a2a7fb782c538c69b58e7f84
4
+ data.tar.gz: 2935c4cbffb05815b2a60e1d1d365f06b166a560a88472d38acaaf0f4d63fd9f
5
5
  SHA512:
6
- metadata.gz: 43555eb980b5cadd73a80c64d5b685fd42193b96e5e57a079410ceaa2cf6a26d29c2e5cef8936c9f3ec7a10138acdd5d4bc6336319a6991d8bc1bb52ebdf4c34
7
- data.tar.gz: f1199002cfb4e1b53d17977d58398de833f13597cf8db5a2093fa2295638686b39ddefaa4891152a65b6130e1d5cda5b37ee5041f75b5fc375ed8dfb9744993d
6
+ metadata.gz: f8ff49b8d4936b46baaeae064f01de7d4a5e78b32579c80e647fb453f37e30ecd03d1f321e29795e89d21b78cddb6e34bd5eaa185b7b30156801976968a7f96b
7
+ data.tar.gz: 1da4e486c2dd4509bfac64af064bea02fce52baeeb904adc11c9f5dd37ddc06d216b1ad5217525a4de6c975128c07ebf69d11e32e20c9698c9da49c9b8fb5bc8
@@ -162,20 +162,26 @@ export const RegularTableView = ({
162
162
  );
163
163
 
164
164
  // Row pinning
165
- function PinnedRow({ row }: { row: Row<any> }) {
165
+ function PinnedRow({ row, position }: { row: Row<any>; position: 'top' | 'bottom' }) {
166
166
  const customRowStyle = getRowStyle(rowStyling, row);
167
+ const bottomRowsCount = table.getBottomRows().length;
168
+
167
169
  return (
168
170
  <tr
169
- className={classnames(
170
- `pinned-row`,
171
- )}
171
+ className={classnames('pinned-row', {
172
+ 'pinned-row-bottom': position === 'bottom',
173
+ })}
172
174
  style={{
173
175
  backgroundColor: customRowStyle?.backgroundColor ? customRowStyle?.backgroundColor : 'white',
174
176
  color: customRowStyle?.fontColor,
175
177
  position: 'sticky',
176
178
  top:
177
- row.getIsPinned() === 'top'
178
- ? `${row.getPinnedIndex() * rowHeight + headerHeight + actionBarHeight}px`
179
+ position === 'top'
180
+ ? `${row.getPinnedIndex() * rowHeight + headerHeight + actionBarHeight}px`
181
+ : undefined,
182
+ bottom:
183
+ position === 'bottom'
184
+ ? `${(bottomRowsCount - 1 - row.getPinnedIndex()) * rowHeight}px`
179
185
  : undefined,
180
186
  zIndex: '3',
181
187
  fontWeight: getFontWeight(customRowStyle),
@@ -201,6 +207,7 @@ export const RegularTableView = ({
201
207
  <>
202
208
  {pinnedRows && table.getTopRows().map((row: Row<GenericObject>) => (
203
209
  <PinnedRow key={row.id}
210
+ position="top"
204
211
  row={row}
205
212
  />
206
213
  ))}
@@ -274,6 +281,12 @@ export const RegularTableView = ({
274
281
  </React.Fragment>
275
282
  );
276
283
  })}
284
+ {pinnedRows && table.getBottomRows().map((row: Row<GenericObject>) => (
285
+ <PinnedRow key={row.id}
286
+ position="bottom"
287
+ row={row}
288
+ />
289
+ ))}
277
290
  </>
278
291
  );
279
292
  }
@@ -72,6 +72,7 @@ export function useTableState({
72
72
  const [localColumnVisibility, setLocalColumnVisibility] = useState({});
73
73
  const [localRowPinning, setLocalRowPinning] = useState<RowPinningState>({
74
74
  top: [],
75
+ bottom: [],
75
76
  });
76
77
  // Manage local state to preserve current page index when inlineRowLoading is enabled so it does not boot table to page 1
77
78
  // We can extend this for more usecases, but for now scoping it only for inlineRowLoading
@@ -235,22 +236,26 @@ export function useTableState({
235
236
  // Handle row pinning changes
236
237
  useEffect(() => {
237
238
  const topPins = pinnedRows?.value?.top ?? [];
238
- if (topPins.length === 0) {
239
- onRowPinningChange({ top: [] });
240
- return;
241
- }
239
+ const bottomPins = pinnedRows?.value?.bottom ?? [];
240
+
242
241
  const rows = table.getRowModel().rows;
243
242
  const collectAllDescendantIds = (subs: Row<GenericObject>[]): string[] =>
244
243
  subs.flatMap(r => [r.id, ...collectAllDescendantIds(r.subRows)]);
245
- const allPinned: string[] = [];
246
- topPins.forEach(id => {
247
- const parent = rows.find(r => r.id === id && r.depth === 0);
248
- if (parent) {
249
- allPinned.push(parent.id, ...collectAllDescendantIds(parent.subRows));
250
- }
244
+ const expandPins = (ids: string[]): string[] => {
245
+ const allPinned: string[] = [];
246
+ ids.forEach(id => {
247
+ const parent = rows.find(r => r.id === id && r.depth === 0);
248
+ if (parent) {
249
+ allPinned.push(parent.id, ...collectAllDescendantIds(parent.subRows));
250
+ }
251
+ });
252
+ return allPinned;
253
+ };
254
+ onRowPinningChange({
255
+ top: expandPins(topPins),
256
+ bottom: expandPins(bottomPins),
251
257
  });
252
- onRowPinningChange({ top: allPinned });
253
- }, [table, pinnedRows?.value?.top?.join(',')]);
258
+ }, [table, pinnedRows?.value?.top?.join(','), pinnedRows?.value?.bottom?.join(',')]);
254
259
 
255
260
  // Set pagination state when pagination is enabled
256
261
  useEffect(() => {
@@ -747,6 +747,9 @@
747
747
  // Row Pinning - React uses inline style; Rails passes same style via html_options from table_body
748
748
  .pinned-row {
749
749
  box-shadow: 0 4px 10px 0 rgba($shadow, 0.16) !important;
750
+ &.pinned-row-bottom {
751
+ box-shadow: 0 -3px 8px 0 rgba($shadow, 0.1) !important;
752
+ }
750
753
  }
751
754
 
752
755
  // For Rails, we can't target the &:last-child since collapsed rows are display: none;
@@ -696,7 +696,7 @@ test("customRenderer prop functions as expected", () => {
696
696
  expect(pill).toBeInTheDocument()
697
697
  })
698
698
 
699
- test("pinnedRows prop renders pinned rows at top", () => {
699
+ test("pinnedRows prop renders top pinned rows at top", () => {
700
700
  const pinnedRowsControl = {
701
701
  value: { top: ["1", "3"] },
702
702
  onChange: jest.fn()
@@ -721,6 +721,31 @@ test("pinnedRows prop renders pinned rows at top", () => {
721
721
  expect(firstPinnedRow).toHaveStyle("background-color: white")
722
722
  })
723
723
 
724
+ test("pinnedRows prop renders bottom pinned rows at bottom", () => {
725
+ const pinnedRowsControl = {
726
+ value: { bottom: ["1", "3"] },
727
+ onChange: jest.fn()
728
+ }
729
+
730
+ render(
731
+ <AdvancedTable
732
+ columnDefinitions={columnDefinitions}
733
+ data={{ testid: testId }}
734
+ pinnedRows={pinnedRowsControl}
735
+ tableData={MOCK_DATA_WITH_ID}
736
+ />
737
+ )
738
+
739
+ const kit = screen.getByTestId(testId)
740
+ const pinnedRows = kit.querySelectorAll(".pinned-row-bottom")
741
+
742
+ expect(pinnedRows).toHaveLength(2)
743
+
744
+ const lastPinnedRow = pinnedRows[0]
745
+ expect(lastPinnedRow).toHaveStyle("position: sticky")
746
+ expect(lastPinnedRow).toHaveStyle("background-color: white")
747
+ })
748
+
724
749
  test("columnStyling.headerAlignment aligns header as expected", () => {
725
750
  const styledColumnDefs = [
726
751
  {
@@ -0,0 +1,57 @@
1
+ import React, { useState } from "react"
2
+ import AdvancedTable from '../_advanced_table'
3
+ import MOCK_DATA_WITH_ID from "./advanced_table_mock_data_with_id.json"
4
+
5
+ const AdvancedTableRowPinningBoth = (props) => {
6
+ const columnDefinitions = [
7
+ {
8
+ accessor: "year",
9
+ label: "Year",
10
+ cellAccessors: ["quarter", "month", "day"],
11
+ },
12
+ {
13
+ accessor: "newEnrollments",
14
+ label: "New Enrollments",
15
+ },
16
+ {
17
+ accessor: "scheduledMeetings",
18
+ label: "Scheduled Meetings",
19
+ },
20
+ {
21
+ accessor: "attendanceRate",
22
+ label: "Attendance Rate",
23
+ },
24
+ {
25
+ accessor: "completedClasses",
26
+ label: "Completed Classes",
27
+ },
28
+ {
29
+ accessor: "classCompletionRate",
30
+ label: "Class Completion Rate",
31
+ },
32
+ {
33
+ accessor: "graduatedStudents",
34
+ label: "Graduated Students",
35
+ },
36
+ ]
37
+
38
+ const [pinnedRows, setPinnedRows] = useState({top: ["8"], bottom: ["1"]})
39
+
40
+ return (
41
+ <div>
42
+ <AdvancedTable
43
+ columnDefinitions={columnDefinitions}
44
+ maxHeight="xs"
45
+ pinnedRows={{value: pinnedRows, onChange: setPinnedRows}}
46
+ tableData={MOCK_DATA_WITH_ID}
47
+ tableProps={{sticky: true}}
48
+ {...props}
49
+ >
50
+ <AdvancedTable.Header enableSorting />
51
+ <AdvancedTable.Body />
52
+ </AdvancedTable>
53
+ </div>
54
+ )
55
+ }
56
+
57
+ export default AdvancedTableRowPinningBoth
@@ -0,0 +1 @@
1
+ This code snippet demonstrates `pinnedRows` taking an array of row ids to both the `top` and `bottom` properties.
@@ -0,0 +1,57 @@
1
+ import React, { useState } from "react"
2
+ import AdvancedTable from '../_advanced_table'
3
+ import MOCK_DATA_WITH_ID from "./advanced_table_mock_data_with_id.json"
4
+
5
+ const AdvancedTableRowPinningBottom = (props) => {
6
+ const columnDefinitions = [
7
+ {
8
+ accessor: "year",
9
+ label: "Year",
10
+ cellAccessors: ["quarter", "month", "day"],
11
+ },
12
+ {
13
+ accessor: "newEnrollments",
14
+ label: "New Enrollments",
15
+ },
16
+ {
17
+ accessor: "scheduledMeetings",
18
+ label: "Scheduled Meetings",
19
+ },
20
+ {
21
+ accessor: "attendanceRate",
22
+ label: "Attendance Rate",
23
+ },
24
+ {
25
+ accessor: "completedClasses",
26
+ label: "Completed Classes",
27
+ },
28
+ {
29
+ accessor: "classCompletionRate",
30
+ label: "Class Completion Rate",
31
+ },
32
+ {
33
+ accessor: "graduatedStudents",
34
+ label: "Graduated Students",
35
+ },
36
+ ]
37
+
38
+ const [pinnedRows, setPinnedRows] = useState({bottom: ["8"]})
39
+
40
+ return (
41
+ <div>
42
+ <AdvancedTable
43
+ columnDefinitions={columnDefinitions}
44
+ maxHeight="xs"
45
+ pinnedRows={{value: pinnedRows, onChange: setPinnedRows}}
46
+ tableData={MOCK_DATA_WITH_ID}
47
+ tableProps={{sticky: true}}
48
+ {...props}
49
+ >
50
+ <AdvancedTable.Header enableSorting />
51
+ <AdvancedTable.Body />
52
+ </AdvancedTable>
53
+ </div>
54
+ )
55
+ }
56
+
57
+ export default AdvancedTableRowPinningBottom
@@ -0,0 +1 @@
1
+ This code snippet demonstrates `pinnedRows` taking an array of row ids to the `bottom` property.
@@ -1,7 +1,7 @@
1
- Use the `pinnedRows` prop to pin specific rows to the top of an Advanced Table. Pinned rows will remain at the top when scrolling through table data and will not change position if sorting is used.
1
+ Use the `pinnedRows` prop to pin specific rows to the top or bottom of an Advanced Table. Pinned rows will remain at the top or bottom when scrolling through table data and will not change position if sorting is used.
2
2
 
3
3
  **NOTE:**
4
4
  - Sticky header required: Pinned rows must be used with `sticky: true` via `tableProps` (works with both responsive and non-responsive tables)
5
5
  - Row ids required: Each object within the `tableData` Array must contain a unique id in order to attach an id to all Rows for this to function.
6
- - `pinnedRows` takes an array of row ids to the `top` property as shown in the code snippet below.
6
+ - `pinnedRows` takes an array of row ids to the `top` or `bottom` property, or both. `top` is shown in the code snippet below.
7
7
  - For expandable rows, use the parent id in `pinnedRows`, all its children will automatically be pinned with it. If id for a child is passed in without parent being pinned, nothing will be pinned.
@@ -67,7 +67,9 @@ examples:
67
67
  - advanced_table_column_headers_custom_cell: Multi-Header Columns with Custom Cells
68
68
  - advanced_table_column_headers_vertical_border: Multi-Header Columns with Vertical Borders
69
69
  - advanced_table_no_subrows: Table with No Subrows or Expansion
70
- - advanced_table_pinned_rows: Pinned Rows
70
+ - advanced_table_pinned_rows: Pinned Rows (Top)
71
+ - advanced_table_pinned_rows_bottom: Pinned Rows (Bottom)
72
+ - advanced_table_pinned_rows_both: Pinned Rows (Both)
71
73
  - advanced_table_selectable_rows: Selectable Rows
72
74
  - advanced_table_selectable_rows_no_subrows_react: Selectable Rows (No Subrows)
73
75
  - advanced_table_selectable_rows_actions: Selectable Rows (With Actions)
@@ -89,5 +91,4 @@ examples:
89
91
  - advanced_table_column_styling_background_multi: Column Styling Background Color with Multiple Headers
90
92
  - advanced_table_padding_control: Padding Control using Column Styling
91
93
  - advanced_table_column_border_color: Column Group Border Color
92
- - advanced_table_fullscreen: Fullscreen
93
94
  - advanced_table_infinite_scroll: Infinite Scroll
@@ -52,4 +52,6 @@ export { default as AdvancedTableColumnStylingBackground } from './_advanced_tab
52
52
  export { default as AdvancedTableColumnStylingBackgroundMulti } from './_advanced_table_column_styling_background_multi.jsx'
53
53
  export { default as AdvancedTableColumnStylingBackgroundCustom } from './_advanced_table_column_styling_background_custom.jsx'
54
54
  export { default as AdvancedTableCascadeCollapse } from './_advanced_table_cascade_collapse.jsx'
55
- export { default as AdvancedTableSortParentOnly } from './_advanced_table_sort_parent_only.jsx'
55
+ export { default as AdvancedTableSortParentOnly } from './_advanced_table_sort_parent_only.jsx'
56
+ export { default as AdvancedTablePinnedRowsBottom } from './_advanced_table_pinned_rows_bottom.jsx'
57
+ export { default as AdvancedTablePinnedRowsBoth } from './_advanced_table_pinned_rows_both.jsx'
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable react/jsx-handler-names */
2
2
  /* eslint-disable react/no-multi-comp */
3
3
 
4
- import React, { useState, useCallback } from "react";
4
+ import React, { useState, useCallback, useContext } from "react";
5
5
  import classnames from "classnames";
6
6
  import Modal from "react-modal";
7
7
 
@@ -17,7 +17,13 @@ import Flex from "../pb_flex/_flex";
17
17
  import IconCircle from "../pb_icon_circle/_icon_circle";
18
18
  import Title from "../pb_title/_title";
19
19
  import { DialogContext, DialogContextValue } from "./_dialog_context";
20
- import { PB_FLOATING_OWNER_ATTR } from "../utilities/floatingPortalHosts";
20
+ import {
21
+ PB_FLOATING_OWNER_ATTR,
22
+ PB_MODAL_ABOVE_FULLSCREEN_Z_INDEX,
23
+ isPortaledFloatingKitInteraction,
24
+ resolveFullscreenOverlayForElement,
25
+ } from "../utilities/floatingPortalHosts";
26
+ import { FullScreenContext } from "../pb_full_screen/context/_full_screen_context";
21
27
 
22
28
  type DialogProps = {
23
29
  aria?: { [key: string]: string };
@@ -95,6 +101,15 @@ const Dialog = (props: DialogProps): React.ReactElement => {
95
101
 
96
102
  const dynamicInlineProps = globalInlineProps(props);
97
103
 
104
+ const { active: isInsideFullscreen } = useContext(FullScreenContext);
105
+
106
+ const modalStyles = {
107
+ overlay: isInsideFullscreen
108
+ ? { zIndex: PB_MODAL_ABOVE_FULLSCREEN_Z_INDEX }
109
+ : undefined,
110
+ content: dynamicInlineProps,
111
+ };
112
+
98
113
  const wrapperClasses = classnames(
99
114
  buildCss("pb_dialog_wrapper"),
100
115
  );
@@ -194,7 +209,7 @@ const Dialog = (props: DialogProps): React.ReactElement => {
194
209
  overlayClassName={overlayClassNames}
195
210
  portalClassName={portalClassName}
196
211
  shouldCloseOnOverlayClick={shouldCloseOnOverlayClick && !loading}
197
- style={{ content: dynamicInlineProps }}
212
+ style={modalStyles}
198
213
  >
199
214
  <div className="pb_dialog_scroll_region">
200
215
  {title && !status ? <Dialog.Header closeable={closeable}>{title}</Dialog.Header> : null}
@@ -0,0 +1,66 @@
1
+ <div id="file-upload-remove-replace">
2
+ <%= pb_rails("list", props: { id: "selected-files-list", margin_bottom: "sm" }) %>
3
+
4
+ <%= pb_rails("file_upload", props: {
5
+ id: "remove_replace",
6
+ input_options: { multiple: true },
7
+ }) %>
8
+ </div>
9
+
10
+ <template id="file-list-item-template">
11
+ <%= pb_rails("list/item") do %>
12
+ <%= pb_rails("flex", props: { align: "center", justify: "between", width: "100%" }) do %>
13
+ <%= pb_rails("body", props: { data: { file_name: true } }) %>
14
+ <%= pb_rails("circle_icon_button", props: {
15
+ icon: "times",
16
+ variant: "secondary",
17
+ input_options: { classname: "remove-file-button" },
18
+ }) %>
19
+ <% end %>
20
+ <% end %>
21
+ </template>
22
+
23
+ <%= javascript_tag defer: "defer" do %>
24
+ document.addEventListener("DOMContentLoaded", function () {
25
+ const fileInput = document.getElementById("upload-remove_replace")
26
+ const list = document.getElementById("selected-files-list")
27
+ const template = document.getElementById("file-list-item-template")
28
+ let files = []
29
+
30
+ if (!fileInput || !list || !template) return
31
+
32
+ function syncInput() {
33
+ const dataTransfer = new DataTransfer()
34
+ files.forEach((file) => dataTransfer.items.add(file))
35
+ fileInput.files = dataTransfer.files
36
+ }
37
+
38
+ function render() {
39
+ list.innerHTML = ""
40
+
41
+ files.forEach((file, index) => {
42
+ const clone = template.content.cloneNode(true)
43
+ const nameElement = clone.querySelector("[data-file-name]")
44
+ const removeButton = clone.querySelector(".remove-file-button")
45
+
46
+ nameElement.textContent = file.name
47
+ removeButton.addEventListener("click", function (event) {
48
+ event.preventDefault()
49
+ event.stopPropagation()
50
+ files = files.filter((_, fileIndex) => fileIndex !== index)
51
+ syncInput()
52
+ render()
53
+ })
54
+
55
+ list.appendChild(clone)
56
+ })
57
+ }
58
+
59
+ fileInput.addEventListener("change", function () {
60
+ const newFiles = Array.from(fileInput.files)
61
+ files = [...files, ...newFiles]
62
+ syncInput()
63
+ render()
64
+ })
65
+ })
66
+ <% end %>
@@ -0,0 +1,53 @@
1
+
2
+ import React, { useState } from 'react'
3
+
4
+ import Body from '../../pb_body/_body'
5
+ import CircleIconButton from '../../pb_circle_icon_button/_circle_icon_button'
6
+ import FileUpload from '../../pb_file_upload/_file_upload'
7
+ import Flex from '../../pb_flex/_flex'
8
+ import List from '../../pb_list/_list'
9
+ import ListItem from '../../pb_list/_list_item'
10
+
11
+ const FileUploadRemoveReplace = (props) => {
12
+ const [filesToUpload, setFilesToUpload] = useState([])
13
+
14
+ const handleOnFilesAccepted = (files) => {
15
+ setFilesToUpload([...filesToUpload, ...files])
16
+ }
17
+
18
+ const handleRemoveFile = (index) => {
19
+ setFilesToUpload(filesToUpload.filter((_, fileIndex) => fileIndex !== index))
20
+ }
21
+
22
+ return (
23
+ <div>
24
+ {filesToUpload.length > 0 && (
25
+ <List marginBottom="sm">
26
+ {filesToUpload.map((file, index) => (
27
+ <ListItem key={`${file.name}-${index}`}>
28
+ <Flex
29
+ align="center"
30
+ justify="between"
31
+ width="100%"
32
+ >
33
+ <Body text={file.name} />
34
+ <CircleIconButton
35
+ aria={{ label: `Remove ${file.name}` }}
36
+ icon="times"
37
+ onClick={() => handleRemoveFile(index)}
38
+ variant="secondary"
39
+ />
40
+ </Flex>
41
+ </ListItem>
42
+ ))}
43
+ </List>
44
+ )}
45
+ <FileUpload
46
+ onFilesAccepted={handleOnFilesAccepted}
47
+ {...props}
48
+ />
49
+ </div>
50
+ )
51
+ }
52
+
53
+ export default FileUploadRemoveReplace
@@ -0,0 +1,5 @@
1
+ The File Upload kit does not include built-in remove or replace behavior. Manage selected files in application state and compose the kit with other Playbook components.
2
+
3
+ **Remove** — Track accepted files in state and filter out the file to remove. In React, pair `List` with `CircleIconButton`. In Rails, listen for `change` on the file input, keep files in a JavaScript array, and use `DataTransfer` to sync the input when a file is removed.
4
+
5
+ **Replace** — Selecting a new file on the native Rails input replaces the current selection. For single-file React workflows, set state to the newly accepted files instead of appending: `setFilesToUpload(files)`.
@@ -4,6 +4,7 @@ examples:
4
4
  - file_upload_default: File Upload
5
5
  - file_upload_custom: Custom
6
6
  - file_upload_error: Error
7
+ - file_upload_remove_replace: Remove and Replace
7
8
 
8
9
  react:
9
10
  - file_upload_default: Default List of files to upload
@@ -11,4 +12,5 @@ examples:
11
12
  - file_upload_custom_message: Add a custom message
12
13
  - file_upload_custom_description: Add your one accepted files description
13
14
  - file_upload_max_size: Set a file size limit
14
- - file_upload_error: Error
15
+ - file_upload_error: Error
16
+ - file_upload_remove_replace: Remove and Replace
@@ -4,3 +4,4 @@ export { default as FileUploadCustomMessage } from './_file_upload_custom_messag
4
4
  export { default as FileUploadCustomDescription } from './_file_upload_custom_description.jsx'
5
5
  export { default as FileUploadMaxSize } from './_file_upload_max_size.jsx'
6
6
  export { default as FileUploadError } from './_file_upload_error.jsx'
7
+ export { default as FileUploadRemoveReplace } from './_file_upload_remove_replace.jsx'
@@ -26,6 +26,8 @@
26
26
  .fullscreen-overlay {
27
27
  background-color: $bg_light;
28
28
  box-sizing: border-box;
29
+ display: flex;
30
+ flex-direction: column;
29
31
  height: 100vh;
30
32
  left: 0;
31
33
  overflow: auto;
@@ -55,6 +57,19 @@
55
57
  .fullscreen-content-sticky {
56
58
  height: calc(100vh - 44px);
57
59
  }
60
+
61
+ .pb_fullscreen_floating_root {
62
+ flex: 0 0 auto;
63
+ min-height: 0;
64
+ pointer-events: none;
65
+ position: relative;
66
+ z-index: 1;
67
+ }
68
+
69
+ .pb_fullscreen_floating_root > * {
70
+ pointer-events: auto;
71
+ }
72
+
58
73
  // Dark mode support
59
74
  .dark {
60
75
  .pb_full_screen_view {
@@ -4,6 +4,7 @@ import { buildAriaProps, buildCss, buildDataProps, buildHtmlProps } from '../uti
4
4
  import { globalProps } from '../utilities/globalProps'
5
5
 
6
6
  import useFullscreen from './useFullScreen'
7
+ import { FullScreenContext } from './context/_full_screen_context'
7
8
 
8
9
  import Flex from '../pb_flex/_flex'
9
10
  import Icon from '../pb_icon/_icon'
@@ -155,15 +156,29 @@ const FullScreenView = (props: FullScreenViewProps) => {
155
156
  const renderContent = () => {
156
157
  if (isFullscreen) {
157
158
  return (
158
- <div className="fullscreen-overlay">
159
- {defaultHeader}
160
- <div className={contentClasses}>
161
- {children}
159
+ <FullScreenContext.Provider value={{ active: true }}>
160
+ <div
161
+ className="fullscreen-overlay"
162
+ data-pb-fullscreen-overlay="true"
163
+ >
164
+ {defaultHeader}
165
+ <div className={contentClasses}>
166
+ {children}
167
+ </div>
168
+ <div
169
+ aria-hidden="true"
170
+ className="pb_fullscreen_floating_root"
171
+ data-pb-dialog-floating-root="true"
172
+ />
162
173
  </div>
163
- </div>
174
+ </FullScreenContext.Provider>
164
175
  )
165
176
  }
166
- return children
177
+ return (
178
+ <FullScreenContext.Provider value={{ active: false }}>
179
+ {children}
180
+ </FullScreenContext.Provider>
181
+ )
167
182
  }
168
183
 
169
184
  return (
@@ -0,0 +1,9 @@
1
+ import React from "react"
2
+
3
+ export type FullScreenContextValue = {
4
+ active: boolean
5
+ }
6
+
7
+ export const FullScreenContext = React.createContext<FullScreenContextValue>({
8
+ active: false,
9
+ })
@@ -90,7 +90,7 @@ const FullScreenTableAndFilter = (props) => {
90
90
  double
91
91
  maxHeight="50vh"
92
92
  minWidth="xs"
93
- popoverProps={{ width: "350px", appendTo: ".fullscreen-overlay" }}
93
+ popoverProps={{ width: "350px" }}
94
94
  results={50}
95
95
  sortOptions={{
96
96
  territory_id: "Territory ID",
@@ -1 +1 @@
1
- Full Screen can host composed workflows such as a filter paired with a data table. For popovers inside the overlay, set the popover append target to `.fullscreen-overlay` so menus render within the fullscreen layer.
1
+ Full Screen can host composed workflows such as a filter paired with a data table. Filter and sort popovers automatically append to the fullscreen overlay; dropdown menus inside them portal above the overlay via the shared floating UI stack.
@@ -115,6 +115,23 @@ test('headerTextStyling can render header text as body text', () => {
115
115
  expect(screen.getByText('Fullscreen Body')).toHaveClass('pb_body_kit')
116
116
  })
117
117
 
118
+ test('fullscreen overlay exposes a floating root for portaled input menus', () => {
119
+ render(
120
+ <FullScreen
121
+ headerText="Fullscreen"
122
+ isFullscreen
123
+ >
124
+ <div>Content</div>
125
+ </FullScreen>
126
+ )
127
+
128
+ const overlay = document.querySelector('.fullscreen-overlay')
129
+ expect(overlay).toBeInTheDocument()
130
+ expect(
131
+ overlay?.querySelector('[data-pb-dialog-floating-root].pb_fullscreen_floating_root')
132
+ ).toBeInTheDocument()
133
+ })
134
+
118
135
  test('isFullscreen opens fullscreen in controlled mode', () => {
119
136
  render(
120
137
  <FullScreen