@codecademy/gamut

72.3.072.3.1-alpha.59371c.0
dist/Form/SelectDropdown/hooks/useNoOptionsAnnouncement.js
+dist/Form/SelectDropdown/hooks/useNoOptionsAnnouncement.jsNew file
+39
Index: package/dist/Form/SelectDropdown/hooks/useNoOptionsAnnouncement.js
===================================================================
--- package/dist/Form/SelectDropdown/hooks/useNoOptionsAnnouncement.js
+++ package/dist/Form/SelectDropdown/hooks/useNoOptionsAnnouncement.js
@@ -0,0 +1,39 @@
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { createNoOptionsMessage } from '../elements';
+const ANNOUNCEMENT_DEBOUNCE_MS = 400;
+/*
+  Announces react-select's "no options" menu text (its default "No options",
+  or a custom `validationMessage`) to screen readers via a standalone live
+  region. react-select's own live region only fires when its `options` prop
+  is non-empty, so it never speaks the "no options" state - this fills that
+  gap, including while a consumer is mid-fetch with an empty `options` array.
+
+  The caller must render `announcement` inside a live-region element that
+  stays mounted for the component's lifetime (no key-based remounting).
+  VoiceOver/Safari only tracks a live region it discovered on first mount -
+  swapping in a new DOM node for every announcement makes it stop hearing
+  updates after the first one. Repeat (even identical) announcements are
+  instead produced by clearing to '' immediately, then setting the real text
+  after a debounce, so the same node gets a genuine, detectable mutation.
+*/
+export const useNoOptionsAnnouncement = () => {
+  const [rawAnnouncement, setRawAnnouncement] = useState('');
+  const [announcement, setAnnouncement] = useState('');
+  useEffect(() => {
+    if (!rawAnnouncement) {
+      setAnnouncement('');
+      return;
+    }
+    const timeoutId = setTimeout(() => {
+      setAnnouncement(rawAnnouncement);
+    }, ANNOUNCEMENT_DEBOUNCE_MS);
+    return () => clearTimeout(timeoutId);
+  }, [rawAnnouncement]);
+  const noOptionsMessageComponent = useMemo(() => createNoOptionsMessage(setRawAnnouncement), []);
+  const clearAnnouncement = useCallback(() => setRawAnnouncement(''), []);
+  return {
+    noOptionsMessageComponent,
+    announcement,
+    clearAnnouncement
+  };
+};
\ No newline at end of file