@planningcenter/topbar 12.5.0-rc.3 → 12.5.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.
@@ -3337,6 +3337,25 @@ function SearchButton({
3337
3337
  }));
3338
3338
  }
3339
3339
 
3340
+ // With the search's own debounce, about 1.4 seconds after the last keystroke,
3341
+ // as in GOV.UK's accessible-autocomplete.
3342
+ const ANNOUNCEMENT_DELAY_MS = 1100;
3343
+ // Screen readers echo typing themselves, as each person has set them to, so
3344
+ // the message waits until typing settles instead of talking over it. Waiting
3345
+ // also lets VoiceOver finish what focus had to say, since it often skips a
3346
+ // change made as focus moves, as when a tab with cached results is selected,
3347
+ // and only the tab someone stops on when arrowing through them is announced.
3348
+ // The region empties at once, so a message that comes back is read again.
3349
+ function useAnnouncement(message) {
3350
+ const [announcement, setAnnouncement] = React__default.useState(null);
3351
+ React__default.useEffect(() => {
3352
+ setAnnouncement(null);
3353
+ const timeout = setTimeout(() => setAnnouncement(message), ANNOUNCEMENT_DELAY_MS);
3354
+ return () => clearTimeout(timeout);
3355
+ }, [message]);
3356
+ return announcement;
3357
+ }
3358
+
3340
3359
  // useId isn't available on React 17, which our peer range still supports.
3341
3360
  let instanceCount$1 = 0;
3342
3361
  function useInstanceId(prefix) {
@@ -4086,7 +4105,18 @@ function subject(token) {
4086
4105
  return undefined;
4087
4106
  }
4088
4107
  }
4089
- async function mintToken(personId) {
4108
+ const minting = new Map();
4109
+ function mintToken(personId) {
4110
+ const running = minting.get(personId);
4111
+ if (running) {
4112
+ return running;
4113
+ } else {
4114
+ const run = mint(personId).finally(() => minting.delete(personId));
4115
+ minting.set(personId, run);
4116
+ return run;
4117
+ }
4118
+ }
4119
+ async function mint(personId) {
4090
4120
  const {
4091
4121
  json,
4092
4122
  ok,
@@ -4110,6 +4140,10 @@ async function mintToken(personId) {
4110
4140
  }
4111
4141
  return token;
4112
4142
  }
4143
+ function currentToken(personId) {
4144
+ var _a;
4145
+ return (_a = cachedToken(personId)) !== null && _a !== void 0 ? _a : mintToken(personId).catch(() => null);
4146
+ }
4113
4147
  async function request(personId, key, init = {}, isRetry = false) {
4114
4148
  var _a;
4115
4149
  const token = isRetry ? await mintToken(personId) : (_a = cachedToken(personId)) !== null && _a !== void 0 ? _a : await mintToken(personId);
@@ -4145,8 +4179,8 @@ function ifMatch(version) {
4145
4179
  // retrying when another tab or device wrote in between. `change` returns null
4146
4180
  // when there's nothing to write. A read-only token (staff impersonating
4147
4181
  // someone) only reads, so nothing is written into the person's history.
4148
- // Resolves to the value now stored, or null when the service couldn't be
4149
- // reached.
4182
+ // Resolves to the value now stored, with whether the token was read-only, or
4183
+ // null when the service couldn't be reached.
4150
4184
  async function updateStoredValue(personId, key, change, attemptsLeft = MAX_ATTEMPTS) {
4151
4185
  const read = await request(personId, key);
4152
4186
  if (read === null || read === void 0 ? void 0 : read.response.ok) {
@@ -4154,9 +4188,15 @@ async function updateStoredValue(personId, key, change, attemptsLeft = MAX_ATTEM
4154
4188
  value,
4155
4189
  version
4156
4190
  } = await read.response.json();
4157
- const next = read.token.readOnly ? null : change(value);
4191
+ const {
4192
+ readOnly
4193
+ } = read.token;
4194
+ const next = readOnly ? null : change(value);
4158
4195
  if (next === null) {
4159
- return Array.isArray(value) ? value : [];
4196
+ return {
4197
+ readOnly,
4198
+ value: Array.isArray(value) ? value : []
4199
+ };
4160
4200
  } else {
4161
4201
  const write = await request(personId, key, {
4162
4202
  body: JSON.stringify({
@@ -4169,7 +4209,10 @@ async function updateStoredValue(personId, key, change, attemptsLeft = MAX_ATTEM
4169
4209
  method: "PUT"
4170
4210
  });
4171
4211
  if (write === null || write === void 0 ? void 0 : write.response.ok) {
4172
- return next;
4212
+ return {
4213
+ readOnly,
4214
+ value: next
4215
+ };
4173
4216
  } else if ((write === null || write === void 0 ? void 0 : write.response.status) === 409 && attemptsLeft > 1) {
4174
4217
  return updateStoredValue(personId, key, change, attemptsLeft - 1);
4175
4218
  } else {
@@ -4221,7 +4264,8 @@ async function syncOnce(scope) {
4221
4264
  return isSameList(mergeEntries(shared, local), shared) ? null : mergeStored(stored, local);
4222
4265
  });
4223
4266
  if (synced) {
4224
- writeRecentlyVisited(scope, mergeEntries(entriesFrom(synced), readRecentlyVisited(scope)));
4267
+ const shared = entriesFrom(synced.value);
4268
+ writeRecentlyVisited(scope, synced.readOnly ? shared : mergeEntries(shared, readRecentlyVisited(scope)));
4225
4269
  }
4226
4270
  } catch {
4227
4271
  // The service is best-effort; the local copy stands on its own.
@@ -4252,11 +4296,23 @@ function sync(scope, followUp) {
4252
4296
  function syncRecentlyVisited(scope) {
4253
4297
  return sync(scope, false);
4254
4298
  }
4299
+ function record(entry, scope, token) {
4300
+ if (!(token === null || token === void 0 ? void 0 : token.readOnly)) {
4301
+ writeRecentlyVisited(scope, addVisit(readRecentlyVisited(scope), entry));
4302
+ if (token) sync(scope, true);
4303
+ }
4304
+ }
4255
4305
  function recordVisit(visit, scope) {
4256
4306
  const entry = toEntry(visit, new Date());
4257
- if (isEntry(entry)) {
4258
- writeRecentlyVisited(scope, addVisit(readRecentlyVisited(scope), entry));
4259
- sync(scope, true);
4307
+ if (!isEntry(entry)) {
4308
+ return Promise.resolve();
4309
+ }
4310
+ const token = isUserStoreSyncUnavailable() ? null : currentToken(scope.personId);
4311
+ if (token instanceof Promise) {
4312
+ return token.then(minted => record(entry, scope, minted));
4313
+ } else {
4314
+ record(entry, scope, token);
4315
+ return Promise.resolve();
4260
4316
  }
4261
4317
  }
4262
4318
  function useRecentlyVisited({
@@ -4937,7 +4993,6 @@ function ProductTabs({
4937
4993
  role: "tablist"
4938
4994
  }, products.map(product => (/*#__PURE__*/React__default.createElement(Tab, {
4939
4995
  "aria-controls": `${id}-panel`,
4940
- "aria-describedby": `${tabId(product.slug)}-description`,
4941
4996
  "aria-selected": product === selected,
4942
4997
  id: tabId(product.slug),
4943
4998
  key: product.slug,
@@ -4955,11 +5010,6 @@ function ProductTabs({
4955
5010
  }), showAllNames || product === selected ? product.appName : (/*#__PURE__*/React__default.createElement("span", {
4956
5011
  className: styles$k.visuallyHidden
4957
5012
  }, product.appName)))))), /*#__PURE__*/React__default.createElement("div", {
4958
- className: styles$k.visuallyHidden
4959
- }, products.map(product => (/*#__PURE__*/React__default.createElement("span", {
4960
- id: `${tabId(product.slug)}-description`,
4961
- key: product.slug
4962
- }, "Planning Center ", product.appName)))), /*#__PURE__*/React__default.createElement("div", {
4963
5013
  "aria-labelledby": tabId(selected.slug),
4964
5014
  className: styles$k.panel,
4965
5015
  id: `${id}-panel`,
@@ -6054,9 +6104,9 @@ function SearchResults({
6054
6104
  });
6055
6105
  case "done":
6056
6106
  if (sections.length === 0) {
6107
+ // The panel's result count announces this, so it isn't read twice.
6057
6108
  return /*#__PURE__*/React__default.createElement(BlankState$1, {
6058
6109
  image: /*#__PURE__*/React__default.createElement(BlankStateNoResults, null),
6059
- role: "status",
6060
6110
  subtitle: "Clear search and try again.",
6061
6111
  title: `No results for “${term.trim()}”`
6062
6112
  });
@@ -6110,6 +6160,7 @@ function SearchPanel({
6110
6160
  highlightedIndex
6111
6161
  } = navigation;
6112
6162
  const inputRef = React__default.useRef(null);
6163
+ const resultCount = useAnnouncement(hasTerm && search.status === "done" ? `${items.length === 0 ? "No results" : `${items.length} ${items.length === 1 ? "result" : "results"}`} for “${term.trim()}” in ${product.appName}` : null);
6113
6164
  const [announceHighlight, setAnnounceHighlight] = React__default.useState(false);
6114
6165
  // The tabs keep focus while arrowing through results, but only the focused
6115
6166
  // input can point screen readers at the highlight, so from the tabs it's
@@ -6168,7 +6219,7 @@ function SearchPanel({
6168
6219
  "aria-live": "polite",
6169
6220
  className: styles$k.visuallyHidden,
6170
6221
  "data-testid": "result-count"
6171
- }, hasTerm && search.status === "done" && items.length > 0 ? `${items.length} ${items.length === 1 ? "result" : "results"} for “${term.trim()}” in ${product.appName}` : null), mobile ? null : /*#__PURE__*/React__default.createElement(KeyboardHints, {
6222
+ }, resultCount), mobile ? null : /*#__PURE__*/React__default.createElement(KeyboardHints, {
6172
6223
  copied: navigation.copied
6173
6224
  }));
6174
6225
  }