@planningcenter/topbar 11.1.2-rc.2 → 11.2.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.
@@ -1901,6 +1901,9 @@ const AppsButton = ({
1901
1901
  ...nativeProps
1902
1902
  }) => {
1903
1903
  return /*#__PURE__*/React__default.createElement(Unbutton, {
1904
+ "aria-expanded": expanded,
1905
+ "aria-haspopup": "true",
1906
+ "aria-label": `App switcher: ${appName}`,
1904
1907
  onClick: onClick,
1905
1908
  className: classnames(styles$9.button, {
1906
1909
  [styles$9["is-expanded"]]: expanded
@@ -1935,6 +1938,72 @@ function getAppsSwitchProductPath(productName) {
1935
1938
  return `${basePath}?${params.toString()}`;
1936
1939
  }
1937
1940
 
1941
+ // Currently scoped to links and buttons since the app switcher only contains
1942
+ // those elements. If this hook is reused in containers with form elements,
1943
+ // expand to include: input, select, textarea, [tabindex]:not([tabindex="-1"])
1944
+ const FOCUSABLE_SELECTOR = "a[href], button:not([disabled])";
1945
+ /**
1946
+ * Manages focus trapping within a container element.
1947
+ *
1948
+ * When `active` becomes true:
1949
+ * - Stores the currently focused element
1950
+ * - Moves focus to the first focusable child
1951
+ *
1952
+ * When `active` becomes false:
1953
+ * - Returns focus to the previously focused element
1954
+ *
1955
+ * While active:
1956
+ * - Tab/Shift+Tab wraps within the container
1957
+ * - Escape calls `onClose`
1958
+ *
1959
+ * Note: focus trapping uses scoped querySelector on the container ref,
1960
+ * which is the same approach used by libraries like focus-trap-react.
1961
+ */
1962
+ function useFocusTrap({
1963
+ active,
1964
+ onClose
1965
+ }) {
1966
+ const containerRef = React__default.useRef(null);
1967
+ const previousFocusRef = React__default.useRef(null);
1968
+ React__default.useLayoutEffect(() => {
1969
+ var _a, _b, _c;
1970
+ if (active) {
1971
+ previousFocusRef.current = document.activeElement;
1972
+ (_b = (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.querySelector(FOCUSABLE_SELECTOR)) === null || _b === void 0 ? void 0 : _b.focus();
1973
+ } else {
1974
+ (_c = previousFocusRef.current) === null || _c === void 0 ? void 0 : _c.focus();
1975
+ previousFocusRef.current = null;
1976
+ }
1977
+ }, [active]);
1978
+ const handleKeyDown = React__default.useCallback(e => {
1979
+ var _a;
1980
+ if (!active) {
1981
+ return;
1982
+ }
1983
+ if (e.key === "Escape") {
1984
+ onClose();
1985
+ return;
1986
+ }
1987
+ if (e.key === "Tab") {
1988
+ const focusable = (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.querySelectorAll(FOCUSABLE_SELECTOR);
1989
+ if (!focusable || focusable.length === 0) return;
1990
+ const first = focusable[0];
1991
+ const last = focusable[focusable.length - 1];
1992
+ if (e.shiftKey && document.activeElement === first) {
1993
+ e.preventDefault();
1994
+ last.focus();
1995
+ } else if (!e.shiftKey && document.activeElement === last) {
1996
+ e.preventDefault();
1997
+ first.focus();
1998
+ }
1999
+ }
2000
+ }, [active, onClose]);
2001
+ return {
2002
+ containerRef,
2003
+ handleKeyDown
2004
+ };
2005
+ }
2006
+
1938
2007
  const fontFamily = {
1939
2008
  fontFamily: `inherit`
1940
2009
  };
@@ -2036,6 +2105,13 @@ const AppsMenu = ({
2036
2105
  const restProducts = [...apps].filter(({
2037
2106
  name
2038
2107
  }) => name !== "Home" && name !== "Accounts").sort((a, b) => a.name.localeCompare(b.name));
2108
+ const {
2109
+ containerRef: menuRef,
2110
+ handleKeyDown
2111
+ } = useFocusTrap({
2112
+ active: visible,
2113
+ onClose: toggle
2114
+ });
2039
2115
  const listItemProps = {
2040
2116
  ...fontFamily,
2041
2117
  alignItems: "center",
@@ -2052,12 +2128,16 @@ const AppsMenu = ({
2052
2128
  className: styles$8.popup,
2053
2129
  isVisible: visible
2054
2130
  }, /*#__PURE__*/React__default.createElement("menu", {
2131
+ ref: menuRef,
2055
2132
  "data-pendo": "app-switcher",
2133
+ onKeyDown: handleKeyDown,
2056
2134
  style: {
2057
2135
  margin: 0,
2058
2136
  padding: 0
2059
2137
  }
2060
2138
  }, homeProduct && (/*#__PURE__*/React__default.createElement(HoverableListItem, {
2139
+ "aria-current": appName === homeProduct.name ? "page" : undefined,
2140
+ "aria-label": homeProduct.name,
2061
2141
  component: "a",
2062
2142
  "data-pendo": `app-switcher--change-app--${homeProduct.name}`,
2063
2143
  "data-turbolinks": false,
@@ -2082,6 +2162,8 @@ const AppsMenu = ({
2082
2162
  const isFirst = i === 0;
2083
2163
  const isLast = i === restProducts.length - 1;
2084
2164
  return /*#__PURE__*/React__default.createElement(HoverableListItem, {
2165
+ "aria-current": name === appName ? "page" : undefined,
2166
+ "aria-label": name,
2085
2167
  component: "a",
2086
2168
  "data-pendo": `app-switcher--change-app--${name}`,
2087
2169
  "data-turbolinks": false,
@@ -2105,6 +2187,8 @@ const AppsMenu = ({
2105
2187
  size: 18
2106
2188
  }));
2107
2189
  }), accountsProduct && (/*#__PURE__*/React__default.createElement(HoverableListItem, {
2190
+ "aria-current": appName === accountsProduct.name ? "page" : undefined,
2191
+ "aria-label": accountsProduct.name,
2108
2192
  component: "a",
2109
2193
  "data-pendo": `app-switcher--change-app--${accountsProduct.name}`,
2110
2194
  "data-turbolinks": false,
@@ -2604,6 +2688,13 @@ const SmallTopbar = ({
2604
2688
  const restProducts = [...props.apps].filter(({
2605
2689
  name
2606
2690
  }) => name !== "Home" && name !== "Accounts");
2691
+ const {
2692
+ containerRef: appMenuRef,
2693
+ handleKeyDown: handleAppMenuKeyDown
2694
+ } = useFocusTrap({
2695
+ active: productSwitcherVisible,
2696
+ onClose: () => setProductSwitcherVisible(false)
2697
+ });
2607
2698
  React__default.useEffect(() => {
2608
2699
  setRoutesMenuVisible(false);
2609
2700
  }, [activeRoute && ((_b = (_a = activeRoute[0]) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.href)]);
@@ -2612,6 +2703,9 @@ const SmallTopbar = ({
2612
2703
  ...props.style
2613
2704
  }
2614
2705
  }, /*#__PURE__*/React__default.createElement("button", {
2706
+ "aria-expanded": productSwitcherVisible,
2707
+ "aria-haspopup": "true",
2708
+ "aria-label": `App switcher: ${props.appName}`,
2615
2709
  type: "button",
2616
2710
  className: styles$3["appmenu-button"],
2617
2711
  onClick: () => {
@@ -2627,6 +2721,7 @@ const SmallTopbar = ({
2627
2721
  }, props.routes)), /*#__PURE__*/React__default.createElement(ClientStorage, {
2628
2722
  item: "Topbar:Small:Menus:Products:visibility",
2629
2723
  render: () => (/*#__PURE__*/React__default.createElement("div", {
2724
+ ref: appMenuRef,
2630
2725
  className: styles$3["appmenu-layer"],
2631
2726
  style: {
2632
2727
  ...(productSwitcherVisible && {
@@ -2635,7 +2730,8 @@ const SmallTopbar = ({
2635
2730
  },
2636
2731
  onClick: () => {
2637
2732
  setProductSwitcherVisible(visible => !visible);
2638
- }
2733
+ },
2734
+ onKeyDown: handleAppMenuKeyDown
2639
2735
  }, /*#__PURE__*/React__default.createElement(CloseMenuButton, null), /*#__PURE__*/React__default.createElement("div", {
2640
2736
  onClick: e => {
2641
2737
  const {
@@ -2647,6 +2743,8 @@ const SmallTopbar = ({
2647
2743
  },
2648
2744
  className: styles$3["appmenu-container"]
2649
2745
  }, /*#__PURE__*/React__default.createElement("a", {
2746
+ "aria-current": props.appName === (homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name) ? "page" : undefined,
2747
+ "aria-label": homeProduct ? homeProduct.name : props.appName,
2650
2748
  "data-pendo": `app-switcher--change-app--${homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name}`,
2651
2749
  "data-turbolinks": false,
2652
2750
  className: classnames(styles$3["appmenu-list-a"], styles$3["appmenu-list-a--top"]),
@@ -2664,6 +2762,8 @@ const SmallTopbar = ({
2664
2762
  key: name,
2665
2763
  className: styles$3["appmenu-list-li"]
2666
2764
  }, /*#__PURE__*/React__default.createElement("a", {
2765
+ "aria-current": name === props.appName ? "page" : undefined,
2766
+ "aria-label": name,
2667
2767
  "data-pendo": `app-switcher--change-app--${name}`,
2668
2768
  "data-turbolinks": false,
2669
2769
  className: styles$3["appmenu-list-a"],