maplibre-gl-basemap-control 0.6.0 → 0.8.0

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
@@ -141,8 +141,10 @@ Mapbox styles include Streets, Outdoors, Light, Dark, Satellite, Satellite Stree
141
141
  and Navigation Night.
142
142
 
143
143
  MapTiler and Amazon Location styles require API keys. Mapbox styles require an access token. Users
144
- can enter keys and tokens in the collapsible provider settings section in the control panel, or you
145
- can provide them when creating the control.
144
+ can enter keys and tokens in the dedicated API keys view, opened from the key button in the panel
145
+ header, or you can provide them when creating the control. When a basemap is selected before its key
146
+ is set, the control surfaces the error and the matching credential field inline, before any
147
+ destructive style change, so the missing key can be entered and the basemap retried with Enter.
146
148
 
147
149
  ```typescript
148
150
  const control = new BasemapControl({
@@ -12,6 +12,7 @@ const DEFAULT_BASEMAP_PROVIDERS = [
12
12
  { id: "maptiler", name: "MapTiler", category: "General" },
13
13
  { id: "nasa-gibs", name: "NASA GIBS", category: "Imagery" },
14
14
  { id: "nlmaps", name: "nlmaps", category: "Regional" },
15
+ { id: "openbasiskaart", name: "Openbasiskaart", category: "Regional" },
15
16
  { id: "openfreemap", name: "OpenFreeMap", category: "Vector Styles" },
16
17
  { id: "openrailwaymap", name: "OpenRailwayMap", category: "Transport" },
17
18
  { id: "openstreetmap", name: "OpenStreetMap", category: "Community" },
@@ -999,6 +1000,21 @@ const DEFAULT_BASEMAPS = [
999
1000
  ],
1000
1001
  tags: ["netherlands", "imagery", "aerial"]
1001
1002
  }),
1003
+ rasterBasemap({
1004
+ id: "openbasiskaart",
1005
+ name: "Openbasiskaart",
1006
+ provider: "openbasiskaart",
1007
+ category: "Regional",
1008
+ description: "Netherlands topographic background map based on OpenStreetMap data.",
1009
+ attribution: "Map data © OpenStreetMap contributors | Openbasiskaart",
1010
+ // The Google Maps Compatible (osm-g) layer uses the standard Web Mercator
1011
+ // grid, so its WMTS {TileMatrix}/{TileRow}/{TileCol} maps directly to xyz.
1012
+ tiles: [
1013
+ "https://www.openbasiskaart.nl/mapcache/wmts/1.0.0/osm-g/default/g/{z}/{y}/{x}.png"
1014
+ ],
1015
+ maxzoom: 18,
1016
+ tags: ["netherlands", "regional", "topographic", "osm", "openbasiskaart"]
1017
+ }),
1002
1018
  {
1003
1019
  id: "openfreemap-positron",
1004
1020
  name: "OpenFreeMap Positron",
@@ -1300,11 +1316,18 @@ class BasemapControl {
1300
1316
  // a traffic overlay reuses a token until it expires instead of creating a new
1301
1317
  // session on every add. Keyed by the serialized config + API key.
1302
1318
  __publicField(this, "_googleSessions", new globalThis.Map());
1303
- __publicField(this, "_providerSettingsCollapsed", true);
1319
+ // Which sub-view the panel content shows: the basemap list, or the dedicated
1320
+ // API-keys settings view opened from the header key button (#837).
1321
+ __publicField(this, "_activeView", "basemaps");
1322
+ __publicField(this, "_settingsButton");
1304
1323
  // Provider of the most recent missing-credential error, used to pick the
1305
- // matching help link. Kept in sync with `_state.error` (both are set together
1306
- // in `_applyBasemapChange`'s catch) so it is never stale when consulted.
1324
+ // matching help link and the inline credential field beside the error. Kept
1325
+ // in sync with `_state.error` (both are cleared together) so it is never
1326
+ // stale when consulted.
1307
1327
  __publicField(this, "_missingCredentialProvider");
1328
+ // The basemap whose application last failed for a missing credential, so the
1329
+ // inline credential field's Enter key can re-attempt it (#837).
1330
+ __publicField(this, "_lastFailedBasemapId");
1308
1331
  __publicField(this, "_resizeHandler", null);
1309
1332
  __publicField(this, "_mapResizeHandler", null);
1310
1333
  __publicField(this, "_resizeAnchor", null);
@@ -1452,39 +1475,39 @@ class BasemapControl {
1452
1475
  }
1453
1476
  setMapTilerApiKey(apiKey) {
1454
1477
  this._mapTilerApiKey = apiKey;
1455
- this._state = { ...this._state, error: void 0 };
1478
+ this._clearError();
1456
1479
  this._renderContent();
1457
1480
  this._emit({ type: "statechange", state: this.getState() });
1458
1481
  }
1459
1482
  setAmazonCredentials(apiKey, awsRegion = this._awsRegion) {
1460
1483
  this._amazonApiKey = apiKey;
1461
1484
  this._awsRegion = awsRegion;
1462
- this._state = { ...this._state, error: void 0 };
1485
+ this._clearError();
1463
1486
  this._renderContent();
1464
1487
  this._emit({ type: "statechange", state: this.getState() });
1465
1488
  }
1466
1489
  setMapboxAccessToken(accessToken) {
1467
1490
  this._mapboxAccessToken = accessToken;
1468
- this._state = { ...this._state, error: void 0 };
1491
+ this._clearError();
1469
1492
  this._renderContent();
1470
1493
  this._emit({ type: "statechange", state: this.getState() });
1471
1494
  }
1472
1495
  setTomTomApiKey(apiKey) {
1473
1496
  this._tomtomApiKey = apiKey;
1474
- this._state = { ...this._state, error: void 0 };
1497
+ this._clearError();
1475
1498
  this._renderContent();
1476
1499
  this._emit({ type: "statechange", state: this.getState() });
1477
1500
  }
1478
1501
  setHereApiKey(apiKey) {
1479
1502
  this._hereApiKey = apiKey;
1480
- this._state = { ...this._state, error: void 0 };
1503
+ this._clearError();
1481
1504
  this._renderContent();
1482
1505
  this._emit({ type: "statechange", state: this.getState() });
1483
1506
  }
1484
1507
  setGoogleMapsApiKey(apiKey) {
1485
1508
  this._googleMapsApiKey = apiKey;
1486
1509
  this._googleSessions.clear();
1487
- this._state = { ...this._state, error: void 0 };
1510
+ this._clearError();
1488
1511
  this._renderContent();
1489
1512
  this._emit({ type: "statechange", state: this.getState() });
1490
1513
  }
@@ -1552,6 +1575,16 @@ class BasemapControl {
1552
1575
  }
1553
1576
  const isOverlay = basemap.source.type === "raster" || basemap.source.type === "vector-overlay";
1554
1577
  const effectiveMode = isOverlay ? mode : "replace";
1578
+ if (!isOverlay) {
1579
+ try {
1580
+ this._resolveStyleUrl(basemap);
1581
+ } catch (cause) {
1582
+ if (cause instanceof MissingCredentialError) {
1583
+ this._enterCredentialError(cause, basemap);
1584
+ throw cause;
1585
+ }
1586
+ }
1587
+ }
1555
1588
  if (!isOverlay && this._state.allowMultiple && this._managedRasters.size > 0 && this._options.confirmStyleReplace) {
1556
1589
  const replacedBasemapIds = [...this._managedRasters.keys()];
1557
1590
  let confirmed = false;
@@ -1605,13 +1638,14 @@ class BasemapControl {
1605
1638
  this._emit({ type: "statechange", state: this.getState() });
1606
1639
  } catch (cause) {
1607
1640
  const error = cause instanceof Error ? cause : new Error(String(cause));
1608
- this._state = { ...this._state, loading: false, error: error.message };
1609
- this._missingCredentialProvider = cause instanceof MissingCredentialError ? cause.provider : void 0;
1610
- if (cause instanceof MissingCredentialError) {
1611
- this._providerSettingsCollapsed = false;
1641
+ if (error instanceof MissingCredentialError) {
1642
+ this._enterCredentialError(error, basemap);
1643
+ } else {
1644
+ this._state = { ...this._state, loading: false, error: error.message };
1645
+ this._missingCredentialProvider = void 0;
1646
+ this._renderContent(true);
1647
+ this._handleError(error, basemap);
1612
1648
  }
1613
- this._renderContent(true);
1614
- this._handleError(error, basemap);
1615
1649
  throw error;
1616
1650
  }
1617
1651
  }
@@ -1674,6 +1708,28 @@ class BasemapControl {
1674
1708
  this._emit({ type: "error", state: this.getState(), error, basemap });
1675
1709
  this._emit({ type: "statechange", state: this.getState() });
1676
1710
  }
1711
+ // Clears the current error and its associated missing-credential provider so
1712
+ // the inline credential field and help link disappear together.
1713
+ _clearError() {
1714
+ if (this._state.error === void 0 && this._missingCredentialProvider === void 0 && this._lastFailedBasemapId === void 0) {
1715
+ return;
1716
+ }
1717
+ this._state = { ...this._state, error: void 0 };
1718
+ this._missingCredentialProvider = void 0;
1719
+ this._lastFailedBasemapId = void 0;
1720
+ }
1721
+ // Records a missing-credential failure: stores the error, remembers the
1722
+ // provider (for the help link and inline field) and the basemap to retry, and
1723
+ // surfaces the basemaps view so the inline credential field beside the error
1724
+ // is visible (#837).
1725
+ _enterCredentialError(error, basemap) {
1726
+ this._state = { ...this._state, loading: false, error: error.message };
1727
+ this._missingCredentialProvider = error.provider;
1728
+ this._lastFailedBasemapId = basemap.id;
1729
+ this._activeView = "basemaps";
1730
+ this._renderContent(true);
1731
+ this._handleError(error, basemap);
1732
+ }
1677
1733
  _createContainer() {
1678
1734
  const container = document.createElement("div");
1679
1735
  container.className = `maplibregl-ctrl maplibregl-ctrl-group basemap-control${this._options.className ? ` ${this._options.className}` : ""}`;
@@ -1708,6 +1764,18 @@ class BasemapControl {
1708
1764
  const title = document.createElement("span");
1709
1765
  title.className = "basemap-control-title";
1710
1766
  title.textContent = this._options.title;
1767
+ const settingsBtn = document.createElement("button");
1768
+ settingsBtn.className = "basemap-control-settings-toggle";
1769
+ settingsBtn.type = "button";
1770
+ settingsBtn.setAttribute("aria-label", "API keys");
1771
+ settingsBtn.title = "API keys";
1772
+ settingsBtn.innerHTML = `
1773
+ <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
1774
+ <path d="m21 2-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0 3 3L22 7l-3-3m-3.5 3.5L19 4"/>
1775
+ </svg>
1776
+ `;
1777
+ settingsBtn.addEventListener("click", () => this._toggleSettingsView());
1778
+ this._settingsButton = settingsBtn;
1711
1779
  const closeBtn = document.createElement("button");
1712
1780
  closeBtn.className = "basemap-control-close";
1713
1781
  closeBtn.type = "button";
@@ -1715,6 +1783,7 @@ class BasemapControl {
1715
1783
  closeBtn.innerHTML = "&times;";
1716
1784
  closeBtn.addEventListener("click", () => this.collapse());
1717
1785
  header.appendChild(title);
1786
+ header.appendChild(settingsBtn);
1718
1787
  header.appendChild(closeBtn);
1719
1788
  this._content = document.createElement("div");
1720
1789
  this._content.className = "basemap-control-content";
@@ -1736,13 +1805,17 @@ class BasemapControl {
1736
1805
  _renderContent(preserveResultsScroll = false) {
1737
1806
  var _a;
1738
1807
  if (!this._content) return;
1808
+ this._reconcileSettingsButton();
1809
+ if (this._activeView === "settings" && this._hasProviderSettings()) {
1810
+ this._content.replaceChildren(this._createSettingsView());
1811
+ return;
1812
+ }
1739
1813
  const categories = getBasemapCategories(this._basemaps);
1740
1814
  const results = this._getFilteredBasemaps();
1741
1815
  const previousResultsScrollTop = preserveResultsScroll ? (_a = this._content.querySelector(".basemap-control-results")) == null ? void 0 : _a.scrollTop : void 0;
1742
1816
  this._content.replaceChildren(
1743
1817
  this._createSearchRow(),
1744
1818
  ...this._options.showMultipleToggle ? [this._createMultipleToggleRow()] : [],
1745
- ...this._hasProviderSettings() ? [this._createProviderSettingsSection()] : [],
1746
1819
  this._createFilterRow(this._providers, categories),
1747
1820
  this._createStatus(results.length),
1748
1821
  this._createResults(results)
@@ -1752,6 +1825,23 @@ class BasemapControl {
1752
1825
  if (resultsElement) resultsElement.scrollTop = previousResultsScrollTop;
1753
1826
  }
1754
1827
  }
1828
+ // Show the key button only when the catalog actually needs a provider
1829
+ // credential, and reflect whether the settings view is currently open.
1830
+ _reconcileSettingsButton() {
1831
+ if (!this._settingsButton) return;
1832
+ const hasSettings = this._hasProviderSettings();
1833
+ this._settingsButton.hidden = !hasSettings;
1834
+ this._settingsButton.classList.toggle(
1835
+ "is-active",
1836
+ hasSettings && this._activeView === "settings"
1837
+ );
1838
+ }
1839
+ _toggleSettingsView() {
1840
+ if (!this._hasProviderSettings()) return;
1841
+ this._activeView = this._activeView === "settings" ? "basemaps" : "settings";
1842
+ this._renderContent();
1843
+ this._emit({ type: "statechange", state: this.getState() });
1844
+ }
1755
1845
  _renderFilteredResults() {
1756
1846
  var _a, _b;
1757
1847
  if (!this._content) return;
@@ -1806,8 +1896,18 @@ class BasemapControl {
1806
1896
  input.value = this._state.query;
1807
1897
  input.setAttribute("aria-label", "Search basemaps");
1808
1898
  input.addEventListener("input", () => {
1899
+ var _a;
1900
+ const hadError = this._state.error !== void 0;
1809
1901
  this._state = { ...this._state, query: input.value };
1810
- this._renderFilteredResults();
1902
+ this._clearError();
1903
+ if (hadError) {
1904
+ this._renderContent();
1905
+ input.focus();
1906
+ const end = input.value.length;
1907
+ (_a = input.setSelectionRange) == null ? void 0 : _a.call(input, end, end);
1908
+ } else {
1909
+ this._renderFilteredResults();
1910
+ }
1811
1911
  this._emit({ type: "statechange", state: this.getState() });
1812
1912
  });
1813
1913
  wrapper.appendChild(input);
@@ -1829,118 +1929,205 @@ class BasemapControl {
1829
1929
  wrapper.appendChild(input);
1830
1930
  return wrapper;
1831
1931
  }
1832
- _createProviderSettingsSection() {
1833
- const details = document.createElement("details");
1834
- details.className = "basemap-control-provider-settings";
1835
- details.open = !this._providerSettingsCollapsed;
1836
- details.addEventListener("toggle", () => {
1837
- this._providerSettingsCollapsed = !details.open;
1838
- });
1839
- const summary = document.createElement("summary");
1840
- summary.className = "basemap-control-provider-settings-summary";
1841
- summary.textContent = "Provider settings";
1842
- details.appendChild(summary);
1843
- const fields = document.createElement("div");
1844
- fields.className = "basemap-control-provider-settings-fields";
1845
- if (this._hasMapTilerBasemaps()) {
1846
- fields.appendChild(
1847
- this._createProviderSettingsInput({
1848
- className: "basemap-control-maptiler-key",
1849
- type: "password",
1850
- placeholder: "MapTiler API key",
1851
- ariaLabel: "MapTiler API key",
1852
- value: this._mapTilerApiKey,
1853
- onInput: (value) => {
1854
- this._mapTilerApiKey = value;
1932
+ // The ordered set of provider credential fields, gated by whether the catalog
1933
+ // actually contains basemaps for each provider. A single source of truth for
1934
+ // both the centralized settings view and the inline field shown beside a
1935
+ // missing-credential error, so the two never drift (#837).
1936
+ _providerFieldDefs() {
1937
+ return [
1938
+ {
1939
+ provider: "maptiler",
1940
+ label: "MapTiler",
1941
+ has: this._hasMapTilerBasemaps(),
1942
+ fields: [
1943
+ {
1944
+ className: "basemap-control-maptiler-key",
1945
+ type: "password",
1946
+ placeholder: "MapTiler API key",
1947
+ ariaLabel: "MapTiler API key",
1948
+ value: this._mapTilerApiKey,
1949
+ onInput: (value) => {
1950
+ this._mapTilerApiKey = value;
1951
+ }
1855
1952
  }
1856
- })
1857
- );
1858
- }
1859
- if (this._hasMapboxBasemaps()) {
1860
- fields.appendChild(
1861
- this._createProviderSettingsInput({
1862
- className: "basemap-control-mapbox-token",
1863
- type: "password",
1864
- placeholder: "Mapbox access token",
1865
- ariaLabel: "Mapbox access token",
1866
- value: this._mapboxAccessToken,
1867
- onInput: (value) => {
1868
- this._mapboxAccessToken = value;
1953
+ ]
1954
+ },
1955
+ {
1956
+ provider: "mapbox",
1957
+ label: "Mapbox",
1958
+ has: this._hasMapboxBasemaps(),
1959
+ fields: [
1960
+ {
1961
+ className: "basemap-control-mapbox-token",
1962
+ type: "password",
1963
+ placeholder: "Mapbox access token",
1964
+ ariaLabel: "Mapbox access token",
1965
+ value: this._mapboxAccessToken,
1966
+ onInput: (value) => {
1967
+ this._mapboxAccessToken = value;
1968
+ }
1869
1969
  }
1870
- })
1871
- );
1872
- }
1873
- if (this._hasTomTomBasemaps()) {
1874
- fields.appendChild(
1875
- this._createProviderSettingsInput({
1876
- className: "basemap-control-tomtom-key",
1877
- type: "password",
1878
- placeholder: "TomTom API key",
1879
- ariaLabel: "TomTom API key",
1880
- value: this._tomtomApiKey,
1881
- onInput: (value) => {
1882
- this._tomtomApiKey = value;
1970
+ ]
1971
+ },
1972
+ {
1973
+ provider: "tomtom",
1974
+ label: "TomTom",
1975
+ has: this._hasTomTomBasemaps(),
1976
+ fields: [
1977
+ {
1978
+ className: "basemap-control-tomtom-key",
1979
+ type: "password",
1980
+ placeholder: "TomTom API key",
1981
+ ariaLabel: "TomTom API key",
1982
+ value: this._tomtomApiKey,
1983
+ onInput: (value) => {
1984
+ this._tomtomApiKey = value;
1985
+ }
1883
1986
  }
1884
- })
1885
- );
1886
- }
1887
- if (this._hasHereBasemaps()) {
1888
- fields.appendChild(
1889
- this._createProviderSettingsInput({
1890
- className: "basemap-control-here-key",
1891
- type: "password",
1892
- placeholder: "HERE API key",
1893
- ariaLabel: "HERE API key",
1894
- value: this._hereApiKey,
1895
- onInput: (value) => {
1896
- this._hereApiKey = value;
1987
+ ]
1988
+ },
1989
+ {
1990
+ provider: "here",
1991
+ label: "HERE",
1992
+ has: this._hasHereBasemaps(),
1993
+ fields: [
1994
+ {
1995
+ className: "basemap-control-here-key",
1996
+ type: "password",
1997
+ placeholder: "HERE API key",
1998
+ ariaLabel: "HERE API key",
1999
+ value: this._hereApiKey,
2000
+ onInput: (value) => {
2001
+ this._hereApiKey = value;
2002
+ }
1897
2003
  }
1898
- })
1899
- );
1900
- }
1901
- if (this._hasGoogleApiKeyBasemaps()) {
1902
- fields.appendChild(
1903
- this._createProviderSettingsInput({
1904
- className: "basemap-control-google-key",
1905
- type: "password",
1906
- placeholder: "Google Maps API key",
1907
- ariaLabel: "Google Maps API key",
1908
- value: this._googleMapsApiKey,
1909
- onInput: (value) => {
1910
- this._googleMapsApiKey = value;
1911
- this._googleSessions.clear();
2004
+ ]
2005
+ },
2006
+ {
2007
+ provider: "google",
2008
+ label: "Google",
2009
+ has: this._hasGoogleApiKeyBasemaps(),
2010
+ fields: [
2011
+ {
2012
+ className: "basemap-control-google-key",
2013
+ type: "password",
2014
+ placeholder: "Google Maps API key",
2015
+ ariaLabel: "Google Maps API key",
2016
+ value: this._googleMapsApiKey,
2017
+ onInput: (value) => {
2018
+ this._googleMapsApiKey = value;
2019
+ this._googleSessions.clear();
2020
+ }
1912
2021
  }
1913
- })
1914
- );
1915
- }
1916
- if (this._hasAmazonBasemaps()) {
1917
- fields.appendChild(
1918
- this._createProviderSettingsInput({
1919
- className: "basemap-control-amazon-key",
1920
- type: "password",
1921
- placeholder: "Amazon API key",
1922
- ariaLabel: "Amazon API key",
1923
- value: this._amazonApiKey,
1924
- onInput: (value) => {
1925
- this._amazonApiKey = value;
2022
+ ]
2023
+ },
2024
+ {
2025
+ provider: "amazon",
2026
+ label: "Amazon",
2027
+ has: this._hasAmazonBasemaps(),
2028
+ fields: [
2029
+ {
2030
+ className: "basemap-control-amazon-key",
2031
+ type: "password",
2032
+ placeholder: "Amazon API key",
2033
+ ariaLabel: "Amazon API key",
2034
+ value: this._amazonApiKey,
2035
+ onInput: (value) => {
2036
+ this._amazonApiKey = value;
2037
+ }
2038
+ },
2039
+ {
2040
+ className: "basemap-control-aws-region",
2041
+ type: "text",
2042
+ placeholder: "AWS region",
2043
+ ariaLabel: "AWS region",
2044
+ value: this._awsRegion,
2045
+ onInput: (value) => {
2046
+ this._awsRegion = value;
2047
+ }
1926
2048
  }
1927
- })
1928
- );
1929
- fields.appendChild(
2049
+ ]
2050
+ }
2051
+ ];
2052
+ }
2053
+ // The dedicated API-keys view, opened from the header key button. It collects
2054
+ // every provider credential in one place, separate from the basemap list, so
2055
+ // the list itself stays free of input clutter (#837, Bug 5).
2056
+ _createSettingsView() {
2057
+ const view = document.createElement("div");
2058
+ view.className = "basemap-control-settings-view";
2059
+ const head = document.createElement("div");
2060
+ head.className = "basemap-control-settings-head";
2061
+ const back = document.createElement("button");
2062
+ back.type = "button";
2063
+ back.className = "basemap-control-settings-back";
2064
+ back.setAttribute("aria-label", "Back to basemaps");
2065
+ back.innerHTML = `
2066
+ <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
2067
+ <path d="m15 18-6-6 6-6"/>
2068
+ </svg>
2069
+ <span>Basemaps</span>
2070
+ `;
2071
+ back.addEventListener("click", () => this._toggleSettingsView());
2072
+ const heading = document.createElement("span");
2073
+ heading.className = "basemap-control-settings-heading";
2074
+ heading.textContent = "API keys";
2075
+ head.appendChild(back);
2076
+ head.appendChild(heading);
2077
+ view.appendChild(head);
2078
+ const intro = document.createElement("p");
2079
+ intro.className = "basemap-control-settings-intro";
2080
+ intro.textContent = "Add the credentials each provider requires to use its basemaps.";
2081
+ view.appendChild(intro);
2082
+ const fields = document.createElement("div");
2083
+ fields.className = "basemap-control-provider-settings-fields";
2084
+ for (const def of this._providerFieldDefs()) {
2085
+ if (def.has) fields.appendChild(this._createProviderFieldGroup(def));
2086
+ }
2087
+ view.appendChild(fields);
2088
+ return view;
2089
+ }
2090
+ _createProviderFieldGroup(def) {
2091
+ const group = document.createElement("div");
2092
+ group.className = `basemap-control-provider-group basemap-control-provider-group-${def.provider}`;
2093
+ const label = document.createElement("span");
2094
+ label.className = "basemap-control-provider-group-label";
2095
+ label.textContent = def.label;
2096
+ group.appendChild(label);
2097
+ for (const field of def.fields) {
2098
+ group.appendChild(this._createProviderSettingsInput(field));
2099
+ }
2100
+ return group;
2101
+ }
2102
+ // Renders the credential field(s) for the provider of the active
2103
+ // missing-credential error, inline beside the error message. Only that one
2104
+ // provider is shown, so the user is not asked to wade through every other
2105
+ // provider's inputs (#837, Bug 3). Returns null when no such error is active.
2106
+ _createInlineCredentialFields() {
2107
+ if (!this._state.error || !this._missingCredentialProvider) return null;
2108
+ const def = this._providerFieldDefs().find(
2109
+ (candidate) => candidate.provider === this._missingCredentialProvider && candidate.has
2110
+ );
2111
+ if (!def) return null;
2112
+ const wrapper = document.createElement("div");
2113
+ wrapper.className = "basemap-control-status-fields";
2114
+ for (const field of def.fields) {
2115
+ wrapper.appendChild(
1930
2116
  this._createProviderSettingsInput({
1931
- className: "basemap-control-aws-region",
1932
- type: "text",
1933
- placeholder: "AWS region",
1934
- ariaLabel: "AWS region",
1935
- value: this._awsRegion,
1936
- onInput: (value) => {
1937
- this._awsRegion = value;
1938
- }
2117
+ ...field,
2118
+ // Keep the error (and this field) on screen while the user types, and
2119
+ // let Enter re-attempt the basemap that failed, so the field is not
2120
+ // yanked out from under the cursor on the first keystroke.
2121
+ rerenderOnInput: false,
2122
+ clearErrorOnInput: false,
2123
+ onEnter: () => this._retryLastFailedBasemap()
1939
2124
  })
1940
2125
  );
1941
2126
  }
1942
- details.appendChild(fields);
1943
- return details;
2127
+ return wrapper;
2128
+ }
2129
+ _retryLastFailedBasemap() {
2130
+ if (this._lastFailedBasemapId) this._selectBasemap(this._lastFailedBasemapId);
1944
2131
  }
1945
2132
  _createProviderSettingsInput({
1946
2133
  className,
@@ -1948,7 +2135,10 @@ class BasemapControl {
1948
2135
  placeholder,
1949
2136
  ariaLabel,
1950
2137
  value,
1951
- onInput
2138
+ onInput,
2139
+ rerenderOnInput = true,
2140
+ clearErrorOnInput = true,
2141
+ onEnter
1952
2142
  }) {
1953
2143
  const wrapper = document.createElement("div");
1954
2144
  wrapper.className = className;
@@ -1965,10 +2155,18 @@ class BasemapControl {
1965
2155
  input.setAttribute("aria-label", ariaLabel);
1966
2156
  input.addEventListener("input", () => {
1967
2157
  onInput(input.value);
1968
- this._state = { ...this._state, error: void 0 };
1969
- this._renderFilteredResults();
2158
+ if (clearErrorOnInput) this._clearError();
2159
+ if (rerenderOnInput) this._renderFilteredResults();
1970
2160
  this._emit({ type: "statechange", state: this.getState() });
1971
2161
  });
2162
+ if (onEnter) {
2163
+ input.addEventListener("keydown", (event) => {
2164
+ if (event.key === "Enter") {
2165
+ event.preventDefault();
2166
+ onEnter();
2167
+ }
2168
+ });
2169
+ }
1972
2170
  wrapper.appendChild(input);
1973
2171
  return wrapper;
1974
2172
  }
@@ -1982,6 +2180,7 @@ class BasemapControl {
1982
2180
  providers.map((provider) => ({ value: provider.id, label: provider.name })),
1983
2181
  (value) => {
1984
2182
  this._state = { ...this._state, providerFilter: value };
2183
+ this._clearError();
1985
2184
  this._renderContent();
1986
2185
  this._emit({ type: "statechange", state: this.getState() });
1987
2186
  }
@@ -1994,6 +2193,7 @@ class BasemapControl {
1994
2193
  categories.map((category) => ({ value: category, label: category })),
1995
2194
  (value) => {
1996
2195
  this._state = { ...this._state, categoryFilter: value };
2196
+ this._clearError();
1997
2197
  this._renderContent();
1998
2198
  this._emit({ type: "statechange", state: this.getState() });
1999
2199
  }
@@ -2043,10 +2243,12 @@ class BasemapControl {
2043
2243
  actions.appendChild(link);
2044
2244
  const hint = document.createElement("span");
2045
2245
  hint.className = "basemap-control-status-hint";
2046
- hint.textContent = ", then add it under Provider settings below.";
2246
+ hint.textContent = ", then enter it below and press Enter.";
2047
2247
  actions.appendChild(hint);
2048
2248
  status.appendChild(actions);
2049
2249
  }
2250
+ const inlineFields = this._createInlineCredentialFields();
2251
+ if (inlineFields) status.appendChild(inlineFields);
2050
2252
  } else {
2051
2253
  status.textContent = `${resultCount} basemap${resultCount === 1 ? "" : "s"}`;
2052
2254
  }
@@ -2619,4 +2821,4 @@ export {
2619
2821
  getBasemapCategories as g,
2620
2822
  resolveBasemapProviders as r
2621
2823
  };
2622
- //# sourceMappingURL=BasemapControl-CnjmO-_2.js.map
2824
+ //# sourceMappingURL=BasemapControl-C9T87uJx.js.map