File size: 4,022 Bytes
9d2d895
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
/**
 * Alternatives tab — ranked bypass sea routes. Arrow keys move selection,
 * Enter highlights the selected corridor on the map via a callback.
 * Proposed corridors shown with a label; unavailable ones are greyed out.
 */

import type {
  BypassCorridorOption,
  GetRouteExplorerLaneResponse,
} from '@/generated/server/worldmonitor/supply_chain/v1/service_server';
import { renderRouteCard } from '../components/RouteCard';
import { setTrustedHtml, trustedHtml } from '@/utils/dom-utils';


export interface AlternativesTabOptions {
  onSelectBypass: (option: BypassCorridorOption) => void;
}

export class AlternativesTab {
  public readonly element: HTMLDivElement;
  private opts: AlternativesTabOptions;
  private seaOptions: BypassCorridorOption[] = [];
  private activeIndex = -1;

  constructor(opts: AlternativesTabOptions) {
    this.opts = opts;
    this.element = document.createElement('div');
    this.element.className = 're-tab re-tab--alternatives';
    this.element.setAttribute('role', 'tabpanel');
    this.element.addEventListener('keydown', this.handleKeydown);
    this.renderEmpty();
  }

  public update(data: GetRouteExplorerLaneResponse | null): void {
    if (!data || data.noModeledLane) {
      this.seaOptions = [];
      this.activeIndex = -1;
      this.renderNoLane();
      return;
    }
    this.seaOptions = data.bypassOptions.filter((o) => o.type !== 'land_bridge');
    this.activeIndex = -1;
    if (this.seaOptions.length === 0) {
      this.renderEmptyAlternatives();
      return;
    }
    this.renderList();
  }

  private renderEmpty(): void {
    setTrustedHtml(this.element, trustedHtml('<div class="re-tab__placeholder">Pick a country pair and product to see alternatives.</div>', "legacy direct innerHTML migration"));
  }

  private renderNoLane(): void {
    setTrustedHtml(this.element, trustedHtml('<div class="re-tab__empty"><p>No modeled lane. Alternatives require a primary route to divert from.</p></div>', "legacy direct innerHTML migration"));
  }

  private renderEmptyAlternatives(): void {
    setTrustedHtml(this.element, trustedHtml('<div class="re-tab__empty"><p>No sea-route alternatives available for this lane\'s primary chokepoint.</p></div>', "legacy direct innerHTML migration"));
  }

  private renderList(): void {
    setTrustedHtml(this.element, trustedHtml('', "legacy direct innerHTML migration"));
    const listEl = document.createElement('div');
    listEl.className = 're-alternatives__list';
    listEl.setAttribute('role', 'listbox');
    listEl.setAttribute('aria-label', 'Alternative sea routes');

    this.seaOptions.forEach((option, idx) => {
      const card = renderRouteCard({
        option,
        index: idx,
        isActive: idx === this.activeIndex,
        onSelect: (o) => {
          this.activeIndex = idx;
          this.renderList();
          this.opts.onSelectBypass(o);
        },
      });
      listEl.append(card);
    });

    this.element.append(listEl);
  }

  private handleKeydown = (e: KeyboardEvent): void => {
    if (this.seaOptions.length === 0) return;
    if (e.key === 'ArrowDown') {
      e.preventDefault();
      const next = Math.min(this.activeIndex + 1, this.seaOptions.length - 1);
      if (next === this.activeIndex) return;
      this.activeIndex = next;
      this.renderList();
      this.focusActive();
    } else if (e.key === 'ArrowUp') {
      e.preventDefault();
      const next = Math.max(this.activeIndex - 1, 0);
      if (next === this.activeIndex) return;
      this.activeIndex = next;
      this.renderList();
      this.focusActive();
    } else if (e.key === 'Enter' && this.activeIndex >= 0) {
      e.preventDefault();
      const option = this.seaOptions[this.activeIndex];
      if (option && option.status !== 'CORRIDOR_STATUS_UNAVAILABLE') {
        this.opts.onSelectBypass(option);
      }
    }
  };

  private focusActive(): void {
    const active = this.element.querySelector('.re-route-card--active') as HTMLElement | null;
    active?.focus();
  }
}