@stack-spot/portal-layout 0.0.62 → 0.0.64

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.
Files changed (40) hide show
  1. package/dist/Layout.d.ts +3 -3
  2. package/dist/Layout.d.ts.map +1 -1
  3. package/dist/Layout.js +9 -6
  4. package/dist/Layout.js.map +1 -1
  5. package/dist/LayoutOverlayManager.d.ts +9 -0
  6. package/dist/LayoutOverlayManager.d.ts.map +1 -1
  7. package/dist/LayoutOverlayManager.js +21 -1
  8. package/dist/LayoutOverlayManager.js.map +1 -1
  9. package/dist/components/error/ErrorDescriptor.d.ts +12 -0
  10. package/dist/components/error/ErrorDescriptor.d.ts.map +1 -0
  11. package/dist/components/error/ErrorDescriptor.js +17 -0
  12. package/dist/components/error/ErrorDescriptor.js.map +1 -0
  13. package/dist/components/menu/MenuContent.d.ts +545 -2
  14. package/dist/components/menu/MenuContent.d.ts.map +1 -1
  15. package/dist/components/menu/useCheckTextOverflow.d.ts +6 -0
  16. package/dist/components/menu/useCheckTextOverflow.d.ts.map +1 -0
  17. package/dist/components/menu/useCheckTextOverflow.js +20 -0
  18. package/dist/components/menu/useCheckTextOverflow.js.map +1 -0
  19. package/dist/components/tour/PortalSwitcherStep.d.ts +3 -0
  20. package/dist/components/tour/PortalSwitcherStep.d.ts.map +1 -0
  21. package/dist/components/tour/PortalSwitcherStep.js +29 -0
  22. package/dist/components/tour/PortalSwitcherStep.js.map +1 -0
  23. package/dist/index.d.ts +0 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +0 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/utils.d.ts +27 -0
  28. package/dist/utils.d.ts.map +1 -1
  29. package/dist/utils.js +50 -0
  30. package/dist/utils.js.map +1 -1
  31. package/package.json +2 -2
  32. package/src/Layout.tsx +11 -8
  33. package/src/LayoutOverlayManager.tsx +17 -1
  34. package/src/components/tour/PortalSwitcherStep.tsx +36 -0
  35. package/src/index.ts +0 -1
  36. package/src/utils.ts +48 -0
  37. package/src/components/tour/Navigation.tsx +0 -29
  38. package/src/components/tour/config.tsx +0 -23
  39. package/src/components/tour/steps/PortalSwitcherStep.tsx +0 -47
  40. package/src/components/tour/utils.tsx +0 -58
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Image, Text } from '@citric/core';
3
+ import { tourStepBuilder } from '@stack-spot/portal-components';
4
+ import { useTranslate } from '@stack-spot/portal-translate';
5
+ export const usePortalSwitcherStep = () => {
6
+ const t = useTranslate(translations);
7
+ return tourStepBuilder({
8
+ selector: '.portal-switcher',
9
+ title: t.title,
10
+ content: _jsxs(_Fragment, { children: [_jsx(Image, { src: "https://marketing.stackspot.com/switch-v2.gif", alt: t.imageAlt }), _jsx(Box, { px: 5, py: 3, children: _jsx(Text, { appearance: "microtext1", colorScheme: "inverse.contrastText", children: t.description }) })] }),
11
+ position: 'right',
12
+ style: {
13
+ width: '300px',
14
+ },
15
+ });
16
+ };
17
+ const translations = {
18
+ en: {
19
+ title: 'Expand Your Horizons with Stackspot',
20
+ description: 'Easily switch between EDP and AI to enhance your projects. Access a wide range of resources with just one click and take your projects to a new level of efficiency. Start your journey now!',
21
+ imageAlt: 'GIF describing how to use product switcher and navigate between AI and EDP portals',
22
+ },
23
+ pt: {
24
+ title: 'Expanda Seus Horizontes com Stackspot',
25
+ description: 'Troque facilmente entre EDP e AI para aprimorar seus projetos. Acesse uma ampla gama de recursos com apenas um clique e leve seus projetos para um novo nível de eficiência. Comece sua jornada agora!',
26
+ imageAlt: 'GIF mostrando como usar o alternador de produtos e navegar entre os portais AI e EDP',
27
+ },
28
+ };
29
+ //# sourceMappingURL=PortalSwitcherStep.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PortalSwitcherStep.js","sourceRoot":"","sources":["../../../src/components/tour/PortalSwitcherStep.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,EAAY,eAAe,EAAE,MAAM,+BAA+B,CAAA;AACzE,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAA;AAC3D,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAa,EAAE;IAClD,MAAM,CAAC,GAAG,YAAY,CAAC,YAAY,CAAC,CAAA;IACpC,OAAO,eAAe,CAAC;QACrB,QAAQ,EAAE,kBAAkB;QAC5B,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,OAAO,EAAE,8BACP,KAAC,KAAK,IAAC,GAAG,EAAC,+CAA+C,EAAC,GAAG,EAAE,CAAC,CAAC,QAAQ,GAAI,EAC9E,KAAC,GAAG,IAAC,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,YACf,KAAC,IAAI,IAAC,UAAU,EAAC,YAAY,EAAC,WAAW,EAAC,sBAAsB,YAAE,CAAC,CAAC,WAAW,GAAQ,GACnF,IACL;QACH,QAAQ,EAAE,OAAO;QACjB,KAAK,EAAE;YACL,KAAK,EAAE,OAAO;SACf;KACF,CAAC,CAAA;AACJ,CAAC,CAAA;AAKD,MAAM,YAAY,GAAG;IACnB,EAAE,EAAE;QACF,KAAK,EAAE,qCAAqC;QAC5C,WAAW,EAAE,8LAA8L;QAC3M,QAAQ,EAAE,oFAAoF;KAC/F;IACD,EAAE,EAAE;QACF,KAAK,EAAE,uCAAuC;QAC9C,WAAW,EAAE,wMAAwM;QACrN,QAAQ,EAAE,sFAAsF;KACjG;CACF,CAAA"}
package/dist/index.d.ts CHANGED
@@ -9,7 +9,6 @@ export { Toaster } from './components/Toaster.js';
9
9
  export { ActionItem, MenuContent, MenuGroup, Title } from './components/menu/MenuContent.js';
10
10
  export { MenuSections } from './components/menu/MenuSections.js';
11
11
  export * from './components/menu/types.js';
12
- export { TextWithPointingArrow } from './components/tour/utils.js';
13
12
  export * from './components/types.js';
14
13
  export * from './elements.js';
15
14
  export * from './errors.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAA;AAChD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAC5C,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAC5D,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAA;AAC1F,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AAC9C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,+BAA+B,CAAA;AACzF,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAA;AAC7D,cAAc,yBAAyB,CAAA;AACvC,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAA;AAC/D,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA;AAC1B,cAAc,UAAU,CAAA;AACxB,OAAO,EAAE,IAAI,IAAI,aAAa,EAAE,MAAM,YAAY,CAAA;AAClD,cAAc,SAAS,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAA;AAChD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAC5C,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAC5D,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAA;AAC1F,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AAC9C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,+BAA+B,CAAA;AACzF,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAA;AAC7D,cAAc,yBAAyB,CAAA;AACvC,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA;AAC1B,cAAc,UAAU,CAAA;AACxB,OAAO,EAAE,IAAI,IAAI,aAAa,EAAE,MAAM,YAAY,CAAA;AAClD,cAAc,SAAS,CAAA"}
package/dist/index.js CHANGED
@@ -9,7 +9,6 @@ export { Toaster } from './components/Toaster.js';
9
9
  export { ActionItem, MenuContent, MenuGroup, Title } from './components/menu/MenuContent.js';
10
10
  export { MenuSections } from './components/menu/MenuSections.js';
11
11
  export * from './components/menu/types.js';
12
- export { TextWithPointingArrow } from './components/tour/utils.js';
13
12
  export * from './components/types.js';
14
13
  export * from './elements.js';
15
14
  export * from './errors.js';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAA;AAChD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAC5C,OAAO,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAA;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAC5D,OAAO,EAAc,aAAa,EAAsB,MAAM,4BAA4B,CAAA;AAC1F,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AAC9C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,+BAA+B,CAAA;AACzF,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAA;AAC7D,cAAc,yBAAyB,CAAA;AACvC,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAA;AAC/D,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA;AAC1B,cAAc,UAAU,CAAA;AACxB,OAAO,EAAE,IAAI,IAAI,aAAa,EAAE,MAAM,YAAY,CAAA;AAClD,cAAc,SAAS,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAA;AAChD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAC5C,OAAO,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAA;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAC5D,OAAO,EAAc,aAAa,EAAsB,MAAM,4BAA4B,CAAA;AAC1F,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AAC9C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,+BAA+B,CAAA;AACzF,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAA;AAC7D,cAAc,yBAAyB,CAAA;AACvC,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA;AAC1B,cAAc,UAAU,CAAA;AACxB,OAAO,EAAE,IAAI,IAAI,aAAa,EAAE,MAAM,YAAY,CAAA;AAClD,cAAc,SAAS,CAAA"}
package/dist/utils.d.ts CHANGED
@@ -22,6 +22,33 @@ interface FocusOptions {
22
22
  */
23
23
  ignore?: string;
24
24
  }
25
+ /**
26
+ * Checks if an element can receive focus.
27
+ *
28
+ * Elements can receive focus only if:
29
+ * - they exist;
30
+ * - they're visible;
31
+ * - they're not disabled;
32
+ * - they are a focusable tag name or have a positive tab index;
33
+ * - they don't have a negative tab index.
34
+ * @param element the element to check.
35
+ * @returns true if the element is focusable, false otherwise.
36
+ */
37
+ export declare function isFocusable(element?: Element | null): boolean;
38
+ /**
39
+ * Focus the element passed as parameter if it can receive focus. Otherwise, decides on another element to focus based on the HTML tree
40
+ * and accessibility attributes.
41
+ *
42
+ * Decision making:
43
+ * - If the element is focusable, focus it.
44
+ * - If the element is not focusable, check if it has an id and if another element controls it (aria-controls).
45
+ * - If the element is controlled by another, try to focus the controller, based in this same algorithm.
46
+ * - Otherwise, try to focus its parent.
47
+ *
48
+ * If no focusable element is found. Nothing happens.
49
+ * @param element the element to focus.
50
+ */
51
+ export declare function focusAccessibleElement(element?: Element | null): void;
25
52
  /**
26
53
  * Focus the first focusable child of the element provided. If the element has no focusable child, nothing happens.
27
54
  *
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAGA,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAIxD;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI,QAWrE;AAED,KAAK,WAAW,GAAG,GAAG,GAAG,QAAQ,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,CAAA;AAC7E,KAAK,kBAAkB,GAAG,WAAW,GAAG,WAAW,EAAE,CAAA;AAErD,UAAU,YAAY;IACpB;;OAEG;IACH,QAAQ,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAChC;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAWD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,WAAW,GAAG,QAAQ,GAAG,IAAI,GAAG,SAAS,EAAE,EAAE,QAAa,EAAE,MAAM,EAAE,GAAE,YAAiB,QA0B/H;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,QAKpC"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAGA,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAIxD;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI,QAWrE;AAED,KAAK,WAAW,GAAG,GAAG,GAAG,QAAQ,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,CAAA;AAC7E,KAAK,kBAAkB,GAAG,WAAW,GAAG,WAAW,EAAE,CAAA;AAErD,UAAU,YAAY;IACpB;;OAEG;IACH,QAAQ,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAChC;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAWD;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,WAYnD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,QAO9D;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,WAAW,GAAG,QAAQ,GAAG,IAAI,GAAG,SAAS,EAAE,EAAE,QAAa,EAAE,MAAM,EAAE,GAAE,YAAiB,QA0B/H;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,QAKpC"}
package/dist/utils.js CHANGED
@@ -38,6 +38,56 @@ const selectors = {
38
38
  textarea: 'select:not(:disabled)',
39
39
  other: '[tabindex]:not([tabindex="-1"])',
40
40
  };
41
+ /**
42
+ * Checks if an element can receive focus.
43
+ *
44
+ * Elements can receive focus only if:
45
+ * - they exist;
46
+ * - they're visible;
47
+ * - they're not disabled;
48
+ * - they are a focusable tag name or have a positive tab index;
49
+ * - they don't have a negative tab index.
50
+ * @param element the element to check.
51
+ * @returns true if the element is focusable, false otherwise.
52
+ */
53
+ export function isFocusable(element) {
54
+ if (!element)
55
+ return false;
56
+ // is disabled: return false
57
+ if (element.ariaDisabled || element.getAttribute('disabled') !== null)
58
+ return false;
59
+ // is invisible: return false
60
+ if (!element.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true }))
61
+ return false;
62
+ // has tab index: return false if negative, true otherwise
63
+ const tabIndexStr = element.getAttribute('tabindex');
64
+ const tabIndex = tabIndexStr ? parseInt(tabIndexStr) : undefined;
65
+ if (tabIndex !== undefined)
66
+ return tabIndex >= 0;
67
+ // check the tag name
68
+ return ['a', 'button', 'input', 'iframe', 'select', 'textarea'].includes(element.tagName.toLowerCase() ?? '');
69
+ }
70
+ /**
71
+ * Focus the element passed as parameter if it can receive focus. Otherwise, decides on another element to focus based on the HTML tree
72
+ * and accessibility attributes.
73
+ *
74
+ * Decision making:
75
+ * - If the element is focusable, focus it.
76
+ * - If the element is not focusable, check if it has an id and if another element controls it (aria-controls).
77
+ * - If the element is controlled by another, try to focus the controller, based in this same algorithm.
78
+ * - Otherwise, try to focus its parent.
79
+ *
80
+ * If no focusable element is found. Nothing happens.
81
+ * @param element the element to focus.
82
+ */
83
+ export function focusAccessibleElement(element) {
84
+ let elementToFocus = element;
85
+ while (elementToFocus && !isFocusable(elementToFocus)) {
86
+ const controlledBy = elementToFocus.id ? document.querySelector(`[aria-controls=${elementToFocus.id}]`) : undefined;
87
+ elementToFocus = (controlledBy ?? elementToFocus.parentElement);
88
+ }
89
+ elementToFocus?.focus?.();
90
+ }
41
91
  /**
42
92
  * Focus the first focusable child of the element provided. If the element has no focusable child, nothing happens.
43
93
  *
package/dist/utils.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stack-spot/portal-layout",
3
- "version": "0.0.62",
3
+ "version": "0.0.64",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -8,9 +8,9 @@
8
8
  "@citric/core": ">=6.0.0",
9
9
  "@citric/icons": ">=5.7.1",
10
10
  "@citric/ui": ">=5.7.2",
11
- "@reactour/tour": ">=3.6.2",
12
11
  "@stack-spot/portal-theme": ">=0.0.11",
13
12
  "@stack-spot/portal-translate": ">=0.0.6",
13
+ "@stack-spot/portal-components": ">=0.0.17",
14
14
  "react": ">=18.2.0",
15
15
  "react-dom": ">=18.2.0",
16
16
  "styled-components": ">=6.1.1"
package/src/Layout.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { ProviderProps, TourProvider } from '@reactour/tour'
1
+ import { TourProps, TourProvider, defaultTourConfig, isNewTourStep } from '@stack-spot/portal-components'
2
2
  import { CSSToCitricAdapter, WithStyle, listToClass } from '@stack-spot/portal-theme'
3
3
  import '@stack-spot/portal-theme/dist/theme.css'
4
4
  import { ReactElement, ReactNode, useEffect } from 'react'
@@ -11,8 +11,7 @@ import { SilentErrorBoundary } from './components/error/SilentErrorBoundary'
11
11
  import { MenuContent } from './components/menu/MenuContent'
12
12
  import { MenuSections } from './components/menu/MenuSections'
13
13
  import { MenuProps } from './components/menu/types'
14
- import { defaultTourConfig } from './components/tour/config'
15
- import { portalSwitcherStep } from './components/tour/steps/PortalSwitcherStep'
14
+ import { usePortalSwitcherStep } from './components/tour/PortalSwitcherStep'
16
15
  import { elementIds, getLayoutElements } from './elements'
17
16
  import { LayoutContext, LayoutProvider } from './layout-context'
18
17
  import './layout.css'
@@ -24,7 +23,7 @@ interface Props extends WithStyle, LayoutContext {
24
23
  extra?: ReactNode,
25
24
  errorDescriptor?: DescriptionFn,
26
25
  onError?: ErrorHandler,
27
- tourProps?: Omit<ProviderProps, 'children'>,
26
+ tourProps?: TourProps,
28
27
  }
29
28
 
30
29
  interface RawProps extends WithStyle, LayoutContext {
@@ -35,7 +34,7 @@ interface RawProps extends WithStyle, LayoutContext {
35
34
  extra?: ReactNode,
36
35
  errorDescriptor?: DescriptionFn,
37
36
  onError?: ErrorHandler,
38
- tourProps?: Omit<ProviderProps, 'children'>,
37
+ tourProps?: TourProps,
39
38
  }
40
39
 
41
40
  export const RawLayout = (
@@ -47,6 +46,7 @@ export const RawLayout = (
47
46
  const { bottomDialog, modal, rightPanel } = overlay.useOverlays()
48
47
  const { layout } = getLayoutElements()
49
48
  const isCompactedOnlyIcons = layout?.classList.contains('menu-compact')
49
+ const portalSwitcherStep = usePortalSwitcherStep()
50
50
 
51
51
  const classes = [
52
52
  menuContent && !isCompactedOnlyIcons ? 'menu-content-visible' : undefined,
@@ -55,8 +55,11 @@ export const RawLayout = (
55
55
  isCompactedOnlyIcons ? 'menu-compact' : undefined,
56
56
  ]
57
57
 
58
- const tourPropsWithDefaults = { ...defaultTourConfig(), ...(tourProps || {}) }
59
- tourPropsWithDefaults.steps.unshift(portalSwitcherStep)
58
+ const tourPropsWithDefaults = { ...defaultTourConfig, ...(tourProps || {}) }
59
+ tourPropsWithDefaults.steps = [
60
+ portalSwitcherStep,
61
+ ...tourPropsWithDefaults.steps,
62
+ ].filter(isNewTourStep)
60
63
 
61
64
  useEffect(() => {
62
65
  if (errorDescriptor) ErrorManager.setDescriptionFunction(errorDescriptor)
@@ -66,7 +69,7 @@ export const RawLayout = (
66
69
  return (
67
70
  <CSSToCitricAdapter>
68
71
  <LayoutProvider anchorTag={anchorTag}>
69
- <TourProvider {...tourPropsWithDefaults} >
72
+ <TourProvider config={tourPropsWithDefaults} >
70
73
  <div id={elementIds.layout} className={listToClass(classes)} style={style}>
71
74
  {children && <div id={elementIds.page}>
72
75
  <article id={elementIds.content}><ErrorBoundary>{children}</ErrorBoundary></article>
@@ -8,7 +8,7 @@ import { getDictionary } from './dictionary'
8
8
  import { LayoutElements, elementIds, getLayoutElements } from './elements'
9
9
  import { ElementNotFound, LayoutError } from './errors'
10
10
  import { showToaster as showReactToaster } from './toaster'
11
- import { focusFirstChild, valueOfLayoutVar } from './utils'
11
+ import { focusAccessibleElement, focusFirstChild, valueOfLayoutVar } from './utils'
12
12
 
13
13
  interface AlertOptions extends Omit<DialogOptions, 'cancel'> {
14
14
  showButton?: boolean,
@@ -49,6 +49,10 @@ class LayoutOverlayManager {
49
49
  private setContent: OverlayContentSetter = {}
50
50
  private elements?: LayoutElements
51
51
  private onModalClose?: () => void
52
+ /**
53
+ * Last element with focus before an overlay is shown.
54
+ */
55
+ private lastActiveElement: Element | null = null
52
56
 
53
57
  private setupElements() {
54
58
  this.elements = getLayoutElements()
@@ -102,6 +106,7 @@ class LayoutOverlayManager {
102
106
  }
103
107
 
104
108
  private showOverlay(element: HTMLElement | null | undefined, extraClasses: string[] = [], blockMainContent = true) {
109
+ this.lastActiveElement = document.activeElement
105
110
  element?.classList.add('visible', ...extraClasses)
106
111
  this.setInteractivity(element, true)
107
112
  if (blockMainContent) this.setMainContentInteractivity(false)
@@ -207,6 +212,15 @@ class LayoutOverlayManager {
207
212
  )
208
213
  }
209
214
 
215
+ /**
216
+ * Focus the element that had focus before the last overlay was opened. If the element is not visible anymore, another one that makes
217
+ * sense (accessibility-wise) is focused.
218
+ */
219
+ private focusLastActiveElement() {
220
+ focusAccessibleElement(this.lastActiveElement)
221
+ this.lastActiveElement = null
222
+ }
223
+
210
224
  closeModal(runCloseListener = true) {
211
225
  this.elements?.modal?.classList.remove('visible')
212
226
  this.elements?.backdrop?.setAttribute('class', '')
@@ -226,6 +240,7 @@ class LayoutOverlayManager {
226
240
  }
227
241
  if (this.setContent.modal) this.setContent.modal(undefined)
228
242
  this.hideOverlay(this.elements?.modal)
243
+ this.focusLastActiveElement()
229
244
  },
230
245
  animationMS,
231
246
  )
@@ -250,6 +265,7 @@ class LayoutOverlayManager {
250
265
  }
251
266
  if (this.setContent.rightPanel) this.setContent.rightPanel(undefined)
252
267
  this.hideOverlay(this.elements?.rightPanel)
268
+ this.focusLastActiveElement()
253
269
  },
254
270
  animationMS,
255
271
  )
@@ -0,0 +1,36 @@
1
+ import { Box, Image, Text } from '@citric/core'
2
+ import { TourStep, tourStepBuilder } from '@stack-spot/portal-components'
3
+ import { useTranslate } from '@stack-spot/portal-translate'
4
+ export const usePortalSwitcherStep = (): TourStep => {
5
+ const t = useTranslate(translations)
6
+ return tourStepBuilder({
7
+ selector: '.portal-switcher',
8
+ title: t.title,
9
+ content: <>
10
+ <Image src="https://marketing.stackspot.com/switch-v2.gif" alt={t.imageAlt} />
11
+ <Box px={5} py={3}>
12
+ <Text appearance="microtext1" colorScheme="inverse.contrastText">{t.description}</Text>
13
+ </Box>
14
+ </>,
15
+ position: 'right',
16
+ style: {
17
+ width: '300px',
18
+ },
19
+ })
20
+ }
21
+
22
+
23
+
24
+
25
+ const translations = {
26
+ en: {
27
+ title: 'Expand Your Horizons with Stackspot',
28
+ description: 'Easily switch between EDP and AI to enhance your projects. Access a wide range of resources with just one click and take your projects to a new level of efficiency. Start your journey now!',
29
+ imageAlt: 'GIF describing how to use product switcher and navigate between AI and EDP portals',
30
+ },
31
+ pt: {
32
+ title: 'Expanda Seus Horizontes com Stackspot',
33
+ description: 'Troque facilmente entre EDP e AI para aprimorar seus projetos. Acesse uma ampla gama de recursos com apenas um clique e leve seus projetos para um novo nível de eficiência. Comece sua jornada agora!',
34
+ imageAlt: 'GIF mostrando como usar o alternador de produtos e navegar entre os portais AI e EDP',
35
+ },
36
+ }
package/src/index.ts CHANGED
@@ -9,7 +9,6 @@ export { Toaster } from './components/Toaster'
9
9
  export { ActionItem, MenuContent, MenuGroup, Title } from './components/menu/MenuContent'
10
10
  export { MenuSections } from './components/menu/MenuSections'
11
11
  export * from './components/menu/types'
12
- export { TextWithPointingArrow } from './components/tour/utils'
13
12
  export * from './components/types'
14
13
  export * from './elements'
15
14
  export * from './errors'
package/src/utils.ts CHANGED
@@ -53,6 +53,54 @@ const selectors: Record<TagPriority, string> = {
53
53
  other: '[tabindex]:not([tabindex="-1"])',
54
54
  }
55
55
 
56
+ /**
57
+ * Checks if an element can receive focus.
58
+ *
59
+ * Elements can receive focus only if:
60
+ * - they exist;
61
+ * - they're visible;
62
+ * - they're not disabled;
63
+ * - they are a focusable tag name or have a positive tab index;
64
+ * - they don't have a negative tab index.
65
+ * @param element the element to check.
66
+ * @returns true if the element is focusable, false otherwise.
67
+ */
68
+ export function isFocusable(element?: Element | null) {
69
+ if (!element) return false
70
+ // is disabled: return false
71
+ if (element.ariaDisabled || element.getAttribute('disabled') !== null) return false
72
+ // is invisible: return false
73
+ if (!element.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })) return false
74
+ // has tab index: return false if negative, true otherwise
75
+ const tabIndexStr = element.getAttribute('tabindex')
76
+ const tabIndex = tabIndexStr ? parseInt(tabIndexStr) : undefined
77
+ if (tabIndex !== undefined) return tabIndex >= 0
78
+ // check the tag name
79
+ return ['a', 'button', 'input', 'iframe', 'select', 'textarea'].includes(element.tagName.toLowerCase() ?? '')
80
+ }
81
+
82
+ /**
83
+ * Focus the element passed as parameter if it can receive focus. Otherwise, decides on another element to focus based on the HTML tree
84
+ * and accessibility attributes.
85
+ *
86
+ * Decision making:
87
+ * - If the element is focusable, focus it.
88
+ * - If the element is not focusable, check if it has an id and if another element controls it (aria-controls).
89
+ * - If the element is controlled by another, try to focus the controller, based in this same algorithm.
90
+ * - Otherwise, try to focus its parent.
91
+ *
92
+ * If no focusable element is found. Nothing happens.
93
+ * @param element the element to focus.
94
+ */
95
+ export function focusAccessibleElement(element?: Element | null) {
96
+ let elementToFocus = element as HTMLElement | null | undefined
97
+ while (elementToFocus && !isFocusable(elementToFocus)) {
98
+ const controlledBy = elementToFocus.id ? document.querySelector(`[aria-controls=${elementToFocus.id}]`) : undefined
99
+ elementToFocus = (controlledBy ?? elementToFocus.parentElement) as HTMLElement | null | undefined
100
+ }
101
+ elementToFocus?.focus?.()
102
+ }
103
+
56
104
  /**
57
105
  * Focus the first focusable child of the element provided. If the element has no focusable child, nothing happens.
58
106
  *
@@ -1,29 +0,0 @@
1
- import { Button, Flex, Text } from '@citric/core'
2
- import { ProviderProps } from '@reactour/tour'
3
- import '@stack-spot/portal-theme/dist/theme.css'
4
- import { useTranslate } from '@stack-spot/portal-translate'
5
- import React from 'react'
6
- import { finishTour } from './utils'
7
-
8
-
9
- type ComponentsDict = Exclude<ProviderProps['components'], undefined>
10
- type NavigationProps = React.ComponentProps<Exclude<ComponentsDict['Navigation'], undefined>>
11
-
12
- export const CustomNavigation = ({ currentStep, steps, setCurrentStep }: NavigationProps) => {
13
- const t = useTranslate({ en: { of: 'of', back: 'back' }, pt: { of: 'de', back: 'voltar' } })
14
- return <Flex w={12} px={5} py={2} mt="-1px" bg="inverse.500" justifyContent="space-between">
15
- <Text appearance="microtext1" colorScheme="inverse.contrastText">{currentStep + 1} {t.of} {steps.length}</Text>
16
- <Flex sx={{ gap: '8px' }}>
17
- {currentStep > 1 &&
18
- <Button sx={{ paddingInline: '20px' }} onClick={() => setCurrentStep(currentStep - 1)} size="sm" colorScheme="light">
19
- {t.back}
20
- </Button>}
21
- <Button sx={{ paddingInline: '20px' }} onClick={() => {
22
- if (currentStep + 1 == steps.length) finishTour()
23
- setCurrentStep(currentStep + 1)
24
- }} size="sm" colorScheme="light">
25
- ok
26
- </Button>
27
- </Flex>
28
- </Flex>
29
- }
@@ -1,23 +0,0 @@
1
- import { ProviderProps } from '@reactour/tour'
2
- import { CustomNavigation } from './Navigation'
3
- import { hasFinishedTour } from './utils'
4
-
5
- export const defaultTourConfig: () => Omit<ProviderProps, 'children'> = () => ({
6
- steps: [],
7
- defaultOpen: !hasFinishedTour(),
8
- showBadge: false,
9
- showDots: false,
10
- showPrevNextButtons: false,
11
- showCloseButton: false,
12
- styles: {
13
- popover: (props) => ({
14
- ...props,
15
- boxShadow: 'unset',
16
- backgroundColor: 'transparent',
17
- padding: '0px 14px', // TODO: find a way to make paddings on y axis so it's possible to use the the pointing arrow.
18
- }),
19
- },
20
- components: {
21
- Navigation: CustomNavigation,
22
- },
23
- })
@@ -1,47 +0,0 @@
1
- import { Box, Button, Flex, IconBox, Image, Text } from '@citric/core'
2
- import { TimesMini } from '@citric/icons'
3
- import { StepType, useTour } from '@reactour/tour'
4
- import { useTranslate } from '@stack-spot/portal-translate'
5
- import { TextWithPointingArrow, finishTour } from '../utils'
6
-
7
- const PortalSwitcherStep = () => {
8
- const t = useTranslate(translations)
9
- const { setIsOpen } = useTour()
10
- return <Box bg="inverse.500">
11
- <TextWithPointingArrow $position="right">
12
- <Flex w={12} pl={5} py={3} flexWrap="nowrap" justifyContent="space-between" alignItems="center">
13
- <Text appearance="body2" colorScheme="inverse.contrastText" weight="medium"> {t.title} </Text>
14
- <Button appearance="text" size="sm" onClick={() => {
15
- finishTour()
16
- setIsOpen(false)
17
- }}>
18
- <IconBox size="xs" colorIcon="inverse.contrastText">
19
- <TimesMini />
20
- </IconBox>
21
- </Button>
22
- </Flex>
23
- </TextWithPointingArrow>
24
- <Image src="https://marketing.stackspot.com/switch-v2.gif"
25
- alt="GIF showing how to move from one portal to another" />
26
- <Box px={5} py={3}>
27
- <Text appearance="microtext1" colorScheme="inverse.contrastText">{t.description}</Text>
28
- </Box>
29
- </Box >
30
- }
31
-
32
- export const portalSwitcherStep: StepType = {
33
- selector: '.portal-switcher',
34
- content: PortalSwitcherStep,
35
- position: 'right',
36
- }
37
-
38
- const translations = {
39
- en: {
40
- title: 'Expand Your Horizons with Stackspot',
41
- description: 'Easily switch between EDP and AI to enhance your projects. Access a wide range of resources with just one click and take your projects to a new level of efficiency. Start your journey now!',
42
- },
43
- pt: {
44
- title: 'Expanda Seus Horizontes com Stackspot',
45
- description: 'Troque facilmente entre EDP e AI para aprimorar seus projetos. Acesse uma ampla gama de recursos com apenas um clique e leve seus projetos para um novo nível de eficiência. Comece sua jornada agora!',
46
- },
47
- }
@@ -1,58 +0,0 @@
1
- import { theme } from '@stack-spot/portal-theme'
2
- import styled from 'styled-components'
3
-
4
- export const TextWithPointingArrow = styled.div < {
5
- $position: 'right' | 'top' | 'left',
6
- $top?: string,
7
- $right?: string,
8
- } > `
9
- position: relative;
10
- width: 100%;
11
- &::after {
12
- content: '';
13
- position: absolute;
14
- border-width: 10px;
15
- border-style: solid;
16
- border-color: transparent;
17
- margin-top: -5px;
18
- border-right-color: ${theme.color.inverse[500]};
19
- ${({ $position, $top }) => $position === 'right' ?
20
- `
21
- top: ${$top || '16px'};
22
- left: -18px;
23
- ` : ''}
24
- ${({ $position, $right }) => $position === 'top' ?
25
- `
26
- bottom: 96%;
27
- right: ${$right || '16px'};
28
- transform: rotate(90deg);
29
- ` : ''}
30
- ${({ $position, $top }) => $position === 'left' ?
31
- `
32
- top: ${$top || '16px'};
33
- right: -18px;
34
- transform: rotate(180deg);
35
- ` : ''}
36
- }
37
- `
38
-
39
- const DOMAIN_REGEX = new RegExp(/(\.*(prd|stg|dev)*.stackspot.com)|localhost/)
40
- const portalUrl = new URL(location.href)
41
- const cookieDomain = DOMAIN_REGEX.exec(portalUrl.host)?.[0]
42
- const defaultCookieAttributes = `domain=${cookieDomain}; SameSite=Strict;`
43
-
44
- const getCookies = (): Record<string, string> => document.cookie.split('; ').reduce((prev, current) => {
45
- const [name, ...value] = current.split('=')
46
- prev[name] = value.join('=')
47
- return prev
48
- }, {} as Record<string, string>)
49
-
50
- const setCookie = (key: string, value: string) => {
51
- document.cookie = `${key}=${value}; ${defaultCookieAttributes}`
52
- }
53
-
54
- const getCookie = (key: string) => getCookies()[key]
55
-
56
- export const finishTour = () => setCookie('welcomeTour', 'done')
57
-
58
- export const hasFinishedTour = () => !!getCookie('welcomeTour')