@planningcenter/topbar 11.1.2-rc.2 → 11.2.0-rc.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/dist/topbar.js CHANGED
@@ -1925,6 +1925,9 @@ const AppsButton = ({
1925
1925
  ...nativeProps
1926
1926
  }) => {
1927
1927
  return /*#__PURE__*/React.createElement(Unbutton, {
1928
+ "aria-expanded": expanded,
1929
+ "aria-haspopup": "true",
1930
+ "aria-label": `App switcher: ${appName}`,
1928
1931
  onClick: onClick,
1929
1932
  className: classnames(styles$9.button, {
1930
1933
  [styles$9["is-expanded"]]: expanded
@@ -1959,6 +1962,72 @@ function getAppsSwitchProductPath(productName) {
1959
1962
  return `${basePath}?${params.toString()}`;
1960
1963
  }
1961
1964
 
1965
+ // Currently scoped to links and buttons since the app switcher only contains
1966
+ // those elements. If this hook is reused in containers with form elements,
1967
+ // expand to include: input, select, textarea, [tabindex]:not([tabindex="-1"])
1968
+ const FOCUSABLE_SELECTOR = "a[href], button:not([disabled])";
1969
+ /**
1970
+ * Manages focus trapping within a container element.
1971
+ *
1972
+ * When `active` becomes true:
1973
+ * - Stores the currently focused element
1974
+ * - Moves focus to the first focusable child
1975
+ *
1976
+ * When `active` becomes false:
1977
+ * - Returns focus to the previously focused element
1978
+ *
1979
+ * While active:
1980
+ * - Tab/Shift+Tab wraps within the container
1981
+ * - Escape calls `onClose`
1982
+ *
1983
+ * Note: focus trapping uses scoped querySelector on the container ref,
1984
+ * which is the same approach used by libraries like focus-trap-react.
1985
+ */
1986
+ function useFocusTrap({
1987
+ active,
1988
+ onClose
1989
+ }) {
1990
+ const containerRef = React.useRef(null);
1991
+ const previousFocusRef = React.useRef(null);
1992
+ React.useLayoutEffect(() => {
1993
+ var _a, _b, _c;
1994
+ if (active) {
1995
+ previousFocusRef.current = document.activeElement;
1996
+ (_b = (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.querySelector(FOCUSABLE_SELECTOR)) === null || _b === void 0 ? void 0 : _b.focus();
1997
+ } else {
1998
+ (_c = previousFocusRef.current) === null || _c === void 0 ? void 0 : _c.focus();
1999
+ previousFocusRef.current = null;
2000
+ }
2001
+ }, [active]);
2002
+ const handleKeyDown = React.useCallback(e => {
2003
+ var _a;
2004
+ if (!active) {
2005
+ return;
2006
+ }
2007
+ if (e.key === "Escape") {
2008
+ onClose();
2009
+ return;
2010
+ }
2011
+ if (e.key === "Tab") {
2012
+ const focusable = (_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.querySelectorAll(FOCUSABLE_SELECTOR);
2013
+ if (!focusable || focusable.length === 0) return;
2014
+ const first = focusable[0];
2015
+ const last = focusable[focusable.length - 1];
2016
+ if (e.shiftKey && document.activeElement === first) {
2017
+ e.preventDefault();
2018
+ last.focus();
2019
+ } else if (!e.shiftKey && document.activeElement === last) {
2020
+ e.preventDefault();
2021
+ first.focus();
2022
+ }
2023
+ }
2024
+ }, [active, onClose]);
2025
+ return {
2026
+ containerRef,
2027
+ handleKeyDown
2028
+ };
2029
+ }
2030
+
1962
2031
  const fontFamily = {
1963
2032
  fontFamily: `inherit`
1964
2033
  };
@@ -2060,6 +2129,13 @@ const AppsMenu = ({
2060
2129
  const restProducts = [...apps].filter(({
2061
2130
  name
2062
2131
  }) => name !== "Home" && name !== "Accounts").sort((a, b) => a.name.localeCompare(b.name));
2132
+ const {
2133
+ containerRef: menuRef,
2134
+ handleKeyDown
2135
+ } = useFocusTrap({
2136
+ active: visible,
2137
+ onClose: toggle
2138
+ });
2063
2139
  const listItemProps = {
2064
2140
  ...fontFamily,
2065
2141
  alignItems: "center",
@@ -2076,12 +2152,16 @@ const AppsMenu = ({
2076
2152
  className: styles$8.popup,
2077
2153
  isVisible: visible
2078
2154
  }, /*#__PURE__*/React.createElement("menu", {
2155
+ ref: menuRef,
2079
2156
  "data-pendo": "app-switcher",
2157
+ onKeyDown: handleKeyDown,
2080
2158
  style: {
2081
2159
  margin: 0,
2082
2160
  padding: 0
2083
2161
  }
2084
2162
  }, homeProduct && (/*#__PURE__*/React.createElement(HoverableListItem, {
2163
+ "aria-current": appName === homeProduct.name ? "page" : undefined,
2164
+ "aria-label": homeProduct.name,
2085
2165
  component: "a",
2086
2166
  "data-pendo": `app-switcher--change-app--${homeProduct.name}`,
2087
2167
  "data-turbolinks": false,
@@ -2106,6 +2186,8 @@ const AppsMenu = ({
2106
2186
  const isFirst = i === 0;
2107
2187
  const isLast = i === restProducts.length - 1;
2108
2188
  return /*#__PURE__*/React.createElement(HoverableListItem, {
2189
+ "aria-current": name === appName ? "page" : undefined,
2190
+ "aria-label": name,
2109
2191
  component: "a",
2110
2192
  "data-pendo": `app-switcher--change-app--${name}`,
2111
2193
  "data-turbolinks": false,
@@ -2129,6 +2211,8 @@ const AppsMenu = ({
2129
2211
  size: 18
2130
2212
  }));
2131
2213
  }), accountsProduct && (/*#__PURE__*/React.createElement(HoverableListItem, {
2214
+ "aria-current": appName === accountsProduct.name ? "page" : undefined,
2215
+ "aria-label": accountsProduct.name,
2132
2216
  component: "a",
2133
2217
  "data-pendo": `app-switcher--change-app--${accountsProduct.name}`,
2134
2218
  "data-turbolinks": false,
@@ -2628,6 +2712,13 @@ const SmallTopbar = ({
2628
2712
  const restProducts = [...props.apps].filter(({
2629
2713
  name
2630
2714
  }) => name !== "Home" && name !== "Accounts");
2715
+ const {
2716
+ containerRef: appMenuRef,
2717
+ handleKeyDown: handleAppMenuKeyDown
2718
+ } = useFocusTrap({
2719
+ active: productSwitcherVisible,
2720
+ onClose: () => setProductSwitcherVisible(false)
2721
+ });
2631
2722
  React.useEffect(() => {
2632
2723
  setRoutesMenuVisible(false);
2633
2724
  }, [activeRoute && ((_b = (_a = activeRoute[0]) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.href)]);
@@ -2636,6 +2727,9 @@ const SmallTopbar = ({
2636
2727
  ...props.style
2637
2728
  }
2638
2729
  }, /*#__PURE__*/React.createElement("button", {
2730
+ "aria-expanded": productSwitcherVisible,
2731
+ "aria-haspopup": "true",
2732
+ "aria-label": `App switcher: ${props.appName}`,
2639
2733
  type: "button",
2640
2734
  className: styles$3["appmenu-button"],
2641
2735
  onClick: () => {
@@ -2651,6 +2745,7 @@ const SmallTopbar = ({
2651
2745
  }, props.routes)), /*#__PURE__*/React.createElement(ClientStorage, {
2652
2746
  item: "Topbar:Small:Menus:Products:visibility",
2653
2747
  render: () => (/*#__PURE__*/React.createElement("div", {
2748
+ ref: appMenuRef,
2654
2749
  className: styles$3["appmenu-layer"],
2655
2750
  style: {
2656
2751
  ...(productSwitcherVisible && {
@@ -2659,7 +2754,8 @@ const SmallTopbar = ({
2659
2754
  },
2660
2755
  onClick: () => {
2661
2756
  setProductSwitcherVisible(visible => !visible);
2662
- }
2757
+ },
2758
+ onKeyDown: handleAppMenuKeyDown
2663
2759
  }, /*#__PURE__*/React.createElement(CloseMenuButton, null), /*#__PURE__*/React.createElement("div", {
2664
2760
  onClick: e => {
2665
2761
  const {
@@ -2671,6 +2767,8 @@ const SmallTopbar = ({
2671
2767
  },
2672
2768
  className: styles$3["appmenu-container"]
2673
2769
  }, /*#__PURE__*/React.createElement("a", {
2770
+ "aria-current": props.appName === (homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name) ? "page" : undefined,
2771
+ "aria-label": homeProduct ? homeProduct.name : props.appName,
2674
2772
  "data-pendo": `app-switcher--change-app--${homeProduct === null || homeProduct === void 0 ? void 0 : homeProduct.name}`,
2675
2773
  "data-turbolinks": false,
2676
2774
  className: classnames(styles$3["appmenu-list-a"], styles$3["appmenu-list-a--top"]),
@@ -2688,6 +2786,8 @@ const SmallTopbar = ({
2688
2786
  key: name,
2689
2787
  className: styles$3["appmenu-list-li"]
2690
2788
  }, /*#__PURE__*/React.createElement("a", {
2789
+ "aria-current": name === props.appName ? "page" : undefined,
2790
+ "aria-label": name,
2691
2791
  "data-pendo": `app-switcher--change-app--${name}`,
2692
2792
  "data-turbolinks": false,
2693
2793
  className: styles$3["appmenu-list-a"],