@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.
@@ -1912,6 +1912,9 @@
1912
1912
  ...nativeProps
1913
1913
  }) => {
1914
1914
  return /*#__PURE__*/React.createElement(Unbutton, {
1915
+ "aria-expanded": expanded,
1916
+ "aria-haspopup": "true",
1917
+ "aria-label": `App switcher: ${appName}`,
1915
1918
  onClick: onClick,
1916
1919
  className: classnames(styles$9.button, {
1917
1920
  [styles$9["is-expanded"]]: expanded
@@ -1946,6 +1949,72 @@
1946
1949
  return `${basePath}?${params.toString()}`;
1947
1950
  }
1948
1951
 
1952
+ // Currently scoped to links and buttons since the app switcher only contains
1953
+ // those elements. If this hook is reused in containers with form elements,
1954
+ // expand to include: input, select, textarea, [tabindex]:not([tabindex="-1"])
1955
+ const FOCUSABLE_SELECTOR = "a[href], button:not([disabled])";
1956
+ /**
1957
+ * Manages focus trapping within a container element.
1958
+ *
1959
+ * When `active` becomes true:
1960
+ * - Stores the currently focused element
1961
+ * - Moves focus to the first focusable child
1962
+ *
1963
+ * When `active` becomes false:
1964
+ * - Returns focus to the previously focused element
1965
+ *
1966
+ * While active:
1967
+ * - Tab/Shift+Tab wraps within the container
1968
+ * - Escape calls `onClose`
1969
+ *
1970
+ * Note: focus trapping uses scoped querySelector on the container ref,
1971
+ * which is the same approach used by libraries like focus-trap-react.
1972
+ */
1973
+ function useFocusTrap({
1974
+ active,
1975
+ onClose
1976
+ }) {
1977
+ const containerRef = React.useRef(null);
1978
+ const previousFocusRef = React.useRef(null);
1979
+ React.useLayoutEffect(() => {
1980
+ var _a, _b, _c;
1981
+ if (active) {
1982
+ previousFocusRef.current = document.activeElement;
1983
+ (_b = (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.querySelector(FOCUSABLE_SELECTOR)) === null || _b === void 0 ? void 0 : _b.focus();
1984
+ } else {
1985
+ (_c = previousFocusRef.current) === null || _c === void 0 ? void 0 : _c.focus();
1986
+ previousFocusRef.current = null;
1987
+ }
1988
+ }, [active]);
1989
+ const handleKeyDown = React.useCallback(e => {
1990
+ var _a;
1991
+ if (!active) {
1992
+ return;
1993
+ }
1994
+ if (e.key === "Escape") {
1995
+ onClose();
1996
+ return;
1997
+ }
1998
+ if (e.key === "Tab") {
1999
+ const focusable = (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.querySelectorAll(FOCUSABLE_SELECTOR);
2000
+ if (!focusable || focusable.length === 0) return;
2001
+ const first = focusable[0];
2002
+ const last = focusable[focusable.length - 1];
2003
+ if (e.shiftKey && document.activeElement === first) {
2004
+ e.preventDefault();
2005
+ last.focus();
2006
+ } else if (!e.shiftKey && document.activeElement === last) {
2007
+ e.preventDefault();
2008
+ first.focus();
2009
+ }
2010
+ }
2011
+ }, [active, onClose]);
2012
+ return {
2013
+ containerRef,
2014
+ handleKeyDown
2015
+ };
2016
+ }
2017
+
1949
2018
  const fontFamily = {
1950
2019
  fontFamily: `inherit`
1951
2020
  };
@@ -2047,6 +2116,13 @@
2047
2116
  const restProducts = [...apps].filter(({
2048
2117
  name
2049
2118
  }) => name !== "Home" && name !== "Accounts").sort((a, b) => a.name.localeCompare(b.name));
2119
+ const {
2120
+ containerRef: menuRef,
2121
+ handleKeyDown
2122
+ } = useFocusTrap({
2123
+ active: visible,
2124
+ onClose: toggle
2125
+ });
2050
2126
  const listItemProps = {
2051
2127
  ...fontFamily,
2052
2128
  alignItems: "center",
@@ -2063,12 +2139,16 @@
2063
2139
  className: styles$8.popup,
2064
2140
  isVisible: visible
2065
2141
  }, /*#__PURE__*/React.createElement("menu", {
2142
+ ref: menuRef,
2066
2143
  "data-pendo": "app-switcher",
2144
+ onKeyDown: handleKeyDown,
2067
2145
  style: {
2068
2146
  margin: 0,
2069
2147
  padding: 0
2070
2148
  }
2071
2149
  }, homeProduct && (/*#__PURE__*/React.createElement(HoverableListItem, {
2150
+ "aria-current": appName === homeProduct.name ? "page" : undefined,
2151
+ "aria-label": homeProduct.name,
2072
2152
  component: "a",
2073
2153
  "data-pendo": `app-switcher--change-app--${homeProduct.name}`,
2074
2154
  "data-turbolinks": false,
@@ -2093,6 +2173,8 @@
2093
2173
  const isFirst = i === 0;
2094
2174
  const isLast = i === restProducts.length - 1;
2095
2175
  return /*#__PURE__*/React.createElement(HoverableListItem, {
2176
+ "aria-current": name === appName ? "page" : undefined,
2177
+ "aria-label": name,
2096
2178
  component: "a",
2097
2179
  "data-pendo": `app-switcher--change-app--${name}`,
2098
2180
  "data-turbolinks": false,
@@ -2116,6 +2198,8 @@
2116
2198
  size: 18
2117
2199
  }));
2118
2200
  }), accountsProduct && (/*#__PURE__*/React.createElement(HoverableListItem, {
2201
+ "aria-current": appName === accountsProduct.name ? "page" : undefined,
2202
+ "aria-label": accountsProduct.name,
2119
2203
  component: "a",
2120
2204
  "data-pendo": `app-switcher--change-app--${accountsProduct.name}`,
2121
2205
  "data-turbolinks": false,
@@ -2615,6 +2699,13 @@
2615
2699
  const restProducts = [...props.apps].filter(({
2616
2700
  name
2617
2701
  }) => name !== "Home" && name !== "Accounts");
2702
+ const {
2703
+ containerRef: appMenuRef,
2704
+ handleKeyDown: handleAppMenuKeyDown
2705
+ } = useFocusTrap({
2706
+ active: productSwitcherVisible,
2707
+ onClose: () => setProductSwitcherVisible(false)
2708
+ });
2618
2709
  React.useEffect(() => {
2619
2710
  setRoutesMenuVisible(false);
2620
2711
  }, [activeRoute && ((_b = (_a = activeRoute[0]) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.href)]);
@@ -2623,6 +2714,9 @@
2623
2714
  ...props.style
2624
2715
  }
2625
2716
  }, /*#__PURE__*/React.createElement("button", {
2717
+ "aria-expanded": productSwitcherVisible,
2718
+ "aria-haspopup": "true",
2719
+ "aria-label": `App switcher: ${props.appName}`,
2626
2720
  type: "button",
2627
2721
  className: styles$3["appmenu-button"],
2628
2722
  onClick: () => {
@@ -2638,6 +2732,7 @@
2638
2732
  }, props.routes)), /*#__PURE__*/React.createElement(ClientStorage, {
2639
2733
  item: "Topbar:Small:Menus:Products:visibility",
2640
2734
  render: () => (/*#__PURE__*/React.createElement("div", {
2735
+ ref: appMenuRef,
2641
2736
  className: styles$3["appmenu-layer"],
2642
2737
  style: {
2643
2738
  ...(productSwitcherVisible && {
@@ -2646,7 +2741,8 @@
2646
2741
  },
2647
2742
  onClick: () => {
2648
2743
  setProductSwitcherVisible(visible => !visible);
2649
- }
2744
+ },
2745
+ onKeyDown: handleAppMenuKeyDown
2650
2746
  }, /*#__PURE__*/React.createElement(CloseMenuButton, null), /*#__PURE__*/React.createElement("div", {
2651
2747
  onClick: e => {
2652
2748
  const {
@@ -2658,6 +2754,8 @@
2658
2754
  },
2659
2755
  className: styles$3["appmenu-container"]
2660
2756
  }, /*#__PURE__*/React.createElement("a", {
2757
+ "aria-current": props.appName === (homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name) ? "page" : undefined,
2758
+ "aria-label": homeProduct ? homeProduct.name : props.appName,
2661
2759
  "data-pendo": `app-switcher--change-app--${homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name}`,
2662
2760
  "data-turbolinks": false,
2663
2761
  className: classnames(styles$3["appmenu-list-a"], styles$3["appmenu-list-a--top"]),
@@ -2675,6 +2773,8 @@
2675
2773
  key: name,
2676
2774
  className: styles$3["appmenu-list-li"]
2677
2775
  }, /*#__PURE__*/React.createElement("a", {
2776
+ "aria-current": name === props.appName ? "page" : undefined,
2777
+ "aria-label": name,
2678
2778
  "data-pendo": `app-switcher--change-app--${name}`,
2679
2779
  "data-turbolinks": false,
2680
2780
  className: styles$3["appmenu-list-a"],