@orangesk/orange-design-system 2.0.0-beta.25 → 2.0.0-beta.26

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  import React from "react";
4
4
  import cx from "classnames";
5
- import { usePathname } from "next/navigation";
5
+ import { usePathname, useRouter } from "next/navigation";
6
6
  import Link from "next/link";
7
7
  import MiniSearch from "minisearch";
8
8
  import { Image } from "../Image";
@@ -231,9 +231,12 @@ export const DocumentationSidebar: React.FC<DocumentationSidebarProps> = (
231
231
  const [isMenuOpenOnMobile, setIsMenuOpenOnMobile] = React.useState(false);
232
232
  const [searchQuery, setSearchQuery] = React.useState("");
233
233
  const [searchResults, setSearchResults] = React.useState<SearchResult[]>([]);
234
+ const [activeSearchIndex, setActiveSearchIndex] = React.useState(0);
234
235
  const [miniSearch, setMiniSearch] = React.useState<MiniSearch | null>(null);
235
236
  const pathname = usePathname();
237
+ const router = useRouter();
236
238
  const sidebarRef = React.useRef<HTMLElement>(null);
239
+ const searchInputRef = React.useRef<HTMLInputElement>(null);
237
240
 
238
241
  // Initialize search on mount
239
242
  React.useEffect(() => {
@@ -274,21 +277,32 @@ export const DocumentationSidebar: React.FC<DocumentationSidebarProps> = (
274
277
  combineWith: "AND", // Require all terms to match
275
278
  }) as unknown as SearchResult[];
276
279
  setSearchResults(results.slice(0, 10)); // Limit to 10 results
280
+ setActiveSearchIndex(0); // Reset active index when results change
277
281
  } else {
278
282
  setSearchResults([]);
283
+ setActiveSearchIndex(0);
279
284
  }
280
285
  };
281
286
 
282
287
  React.useEffect(() => {
283
- const handleEsc = (event: KeyboardEvent) => {
288
+ const handleKeyDown = (event: KeyboardEvent) => {
289
+ // Handle Cmd+K or Ctrl+K to focus search
290
+ if ((event.metaKey || event.ctrlKey) && event.key === "k") {
291
+ event.preventDefault();
292
+ searchInputRef.current?.focus();
293
+ return;
294
+ }
295
+
296
+ // Handle Escape key
284
297
  if (event.key === "Escape") {
285
298
  setIsMenuOpenOnMobile(false);
286
299
  setSearchQuery("");
287
300
  setSearchResults([]);
301
+ setActiveSearchIndex(0);
288
302
  }
289
303
  };
290
- document.addEventListener("keydown", handleEsc);
291
- return () => document.removeEventListener("keydown", handleEsc);
304
+ document.addEventListener("keydown", handleKeyDown);
305
+ return () => document.removeEventListener("keydown", handleKeyDown);
292
306
  }, []);
293
307
 
294
308
  // Scroll to active item when pathname changes or component mounts
@@ -311,6 +325,36 @@ export const DocumentationSidebar: React.FC<DocumentationSidebarProps> = (
311
325
  setIsMenuOpenOnMobile(false);
312
326
  setSearchQuery("");
313
327
  setSearchResults([]);
328
+ setActiveSearchIndex(0);
329
+ };
330
+
331
+ // Handle keyboard navigation in search results
332
+ const handleSearchKeyDown = (
333
+ event: React.KeyboardEvent<HTMLInputElement>,
334
+ ) => {
335
+ if (searchResults.length === 0) return;
336
+
337
+ switch (event.key) {
338
+ case "ArrowDown":
339
+ event.preventDefault();
340
+ setActiveSearchIndex((prev) =>
341
+ prev < searchResults.length - 1 ? prev + 1 : prev,
342
+ );
343
+ break;
344
+ case "ArrowUp":
345
+ event.preventDefault();
346
+ setActiveSearchIndex((prev) => (prev > 0 ? prev - 1 : 0));
347
+ break;
348
+ case "Enter":
349
+ event.preventDefault();
350
+ if (searchResults[activeSearchIndex]) {
351
+ const href = searchResults[activeSearchIndex].href;
352
+ searchInputRef.current?.blur();
353
+ router.push(href);
354
+ handleSelect(href);
355
+ }
356
+ break;
357
+ }
314
358
  };
315
359
 
316
360
  const classes = cx(CLASS_ROOT, className, {
@@ -341,11 +385,13 @@ export const DocumentationSidebar: React.FC<DocumentationSidebarProps> = (
341
385
  <TextInput
342
386
  id="search-documentation"
343
387
  htmlType="search"
344
- placeholder="Search documentation..."
388
+ placeholder="CTRL+K to search..."
345
389
  value={searchQuery}
346
390
  onChange={(e) => handleSearch(e.currentTarget.value)}
391
+ onKeyDown={handleSearchKeyDown}
347
392
  width="fullwidth"
348
393
  aria-label="Search documentation"
394
+ ref={searchInputRef}
349
395
  />
350
396
  {searchQuery && (
351
397
  <button
@@ -365,11 +411,14 @@ export const DocumentationSidebar: React.FC<DocumentationSidebarProps> = (
365
411
  {searchResults.length > 0 && (
366
412
  <div className={`${CLASS_ROOT}__search-results`}>
367
413
  <ul className={`${CLASS_ROOT}__search-list`}>
368
- {searchResults.map((result) => (
414
+ {searchResults.map((result, index) => (
369
415
  <li key={result.id}>
370
416
  <Link
371
417
  href={result.href}
372
- className={`${CLASS_ROOT}__search-result-link`}
418
+ className={cx(`${CLASS_ROOT}__search-result-link`, {
419
+ [`${CLASS_ROOT}__search-result-link--active`]:
420
+ index === activeSearchIndex,
421
+ })}
373
422
  onClick={() => handleSelect(result.href)}
374
423
  >
375
424
  {result.label}
@@ -268,6 +268,12 @@
268
268
  color: var(--color-text-default);
269
269
  }
270
270
 
271
+ &--active {
272
+ background-color: var(--color-surface-subtle);
273
+ color: var(--color-text-default);
274
+ font-weight: 600;
275
+ }
276
+
271
277
  &:focus-visible {
272
278
  outline: 2px solid var(--color-border-accent);
273
279
  outline-offset: -2px;
@@ -10,6 +10,8 @@ interface ModalConfig {
10
10
  root: string;
11
11
  /** Move modal into this element selector (must be unique in DOM) */
12
12
  modalsRoot: string;
13
+ /** Disable moving modal into #root-modals */
14
+ disablePortal?: boolean;
13
15
  }
14
16
 
15
17
  const defaultConfig = (): ModalConfig => ({
@@ -111,7 +113,7 @@ export default class Modal {
111
113
  }
112
114
 
113
115
  init(): void {
114
- if (this.config.modalsRoot) {
116
+ if (this.config.modalsRoot && !this.config.disablePortal) {
115
117
  Modal.moveToModalRoot(
116
118
  this.element,
117
119
  document.querySelector(this.config.modalsRoot),
@@ -83,7 +83,8 @@ const Modal: React.FC<ModalProps> = ({
83
83
  disableFooterSpacing,
84
84
  ...other
85
85
  }) => {
86
- const [modalRef, instance] = useStatic(ModalStatic);
86
+ const modalConfig = isActive !== undefined ? { disablePortal: true } : {};
87
+ const [modalRef, instance] = useStatic(ModalStatic, modalConfig);
87
88
 
88
89
  useEffect(() => {
89
90
  if (isActive && instance && (instance as any).current) {