Closes #1944 ## Changes - Pagination stores page number in url search params, rather than internal state, allowing going back to a specific page in a list - Pagination navigation pushes to history stack, and listens to history changes to be able to respond to browser history navigation (back/forward) - Search parameter reactive controller powers pagination component - Pagination component allows for multiple simultaneous paginations via custom `name` property ## Manual testing 1. Log in as any role 2. Go to one of the list views on an org with enough items in the list to span more than one page 3. Click on one of the pages, and navigate back in your browser. The selected page should respect this navigation and return to the initial numbered page. 4. Navigate forward in your browser. The selected page should respect this navigation and switch to the numbered page from the previous step. 5. Click on a non-default page, and then click on one of the items in the list to go to its detail page. Then, using your browser's back button, return to the list page. You should be on the same numbered page as before. --------- Co-authored-by: sua yoo <sua@suayoo.com>
		
			
				
	
	
		
			58 lines
		
	
	
		
			1.6 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			58 lines
		
	
	
		
			1.6 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import type { ReactiveController, ReactiveControllerHost } from "lit";
 | |
| 
 | |
| export class SearchParamsController implements ReactiveController {
 | |
|   private readonly host: ReactiveControllerHost;
 | |
|   private readonly changeHandler?: (
 | |
|     searchParams: URLSearchParams,
 | |
|     prevParams: URLSearchParams,
 | |
|   ) => void;
 | |
|   private prevParams = new URLSearchParams(location.search);
 | |
| 
 | |
|   public get searchParams() {
 | |
|     return new URLSearchParams(location.search);
 | |
|   }
 | |
| 
 | |
|   public set(
 | |
|     update: URLSearchParams | ((prev: URLSearchParams) => URLSearchParams),
 | |
|     options: { replace?: boolean; data?: unknown } = { replace: false },
 | |
|   ) {
 | |
|     this.prevParams = new URLSearchParams(this.searchParams);
 | |
|     const url = new URL(location.toString());
 | |
|     url.search =
 | |
|       typeof update === "function"
 | |
|         ? update(this.searchParams).toString()
 | |
|         : update.toString();
 | |
| 
 | |
|     if (options.replace) {
 | |
|       history.replaceState(options.data, "", url);
 | |
|     } else {
 | |
|       history.pushState(options.data, "", url);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   constructor(
 | |
|     host: ReactiveControllerHost,
 | |
|     onChange?: (
 | |
|       searchParams: URLSearchParams,
 | |
|       prevParams: URLSearchParams,
 | |
|     ) => void,
 | |
|   ) {
 | |
|     this.host = host;
 | |
|     host.addController(this);
 | |
|     this.changeHandler = onChange;
 | |
|   }
 | |
| 
 | |
|   hostConnected(): void {
 | |
|     window.addEventListener("popstate", this.onPopState);
 | |
|   }
 | |
| 
 | |
|   hostDisconnected(): void {
 | |
|     window.removeEventListener("popstate", this.onPopState);
 | |
|   }
 | |
| 
 | |
|   private readonly onPopState = (_e: PopStateEvent) => {
 | |
|     this.changeHandler?.(this.searchParams, this.prevParams);
 | |
|     this.prevParams = new URLSearchParams(this.searchParams);
 | |
|   };
 | |
| }
 |