@khanacademy/wonder-blocks-tabs 0.1.0 → 0.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @khanacademy/wonder-blocks-tabs
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 27f6298: `NavigationTabs`: Add `animated` prop to enable transition animation. Defaults to false.
8
+
9
+ ## 0.1.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 70fbe23: Update ts config
14
+
3
15
  ## 0.1.0
4
16
 
5
17
  ### Minor Changes
@@ -0,0 +1,63 @@
1
+ import { StyleType } from "@khanacademy/wonder-blocks-core";
2
+ import * as React from "react";
3
+ type NavigationTabItemLinkProps = {
4
+ style: StyleType;
5
+ "aria-current"?: "page";
6
+ };
7
+ /**
8
+ * A component for a tab item in NavigationTabs. It is used with a Link
9
+ * component.
10
+ *
11
+ * ## Usage
12
+ *
13
+ * ```jsx
14
+ * import {NavigationTab, NavigationTabItem} from "@khanacademy/wonder-blocks-tabs";
15
+ * import Link from "@khanacademy/wonder-blocks-link";
16
+ *
17
+ * <NavigationTabs>
18
+ * <NavigationTabItem>
19
+ * <Link href="/link-1">Link 1</Link>
20
+ * </NavigationTabItem>
21
+ * <NavigationTabItem>
22
+ * <Link href="/link-2">Link 2</Link>
23
+ * </NavigationTabItem>
24
+ * </NavigationTabs>
25
+ * ```
26
+ */
27
+ export declare const NavigationTabItem: React.ForwardRefExoticComponent<Readonly<import("@khanacademy/wonder-blocks-core").AriaAttributes> & Readonly<{
28
+ role?: import("@khanacademy/wonder-blocks-core").AriaRole;
29
+ }> & {
30
+ /**
31
+ * The `Link` to render for the navigation tab item.
32
+ *
33
+ * When a `Link` component is passed in for the `children` prop,
34
+ * `NavigationTabItem` will inject props for the `Link`. For specific use
35
+ * cases where the `Link` component is wrapped by another component (like a
36
+ * `Tooltip` or `Popover`), a render function can be used instead. The
37
+ * render function provides the Link props that should be applied to the
38
+ * Link component. See example in the docs for more details.
39
+ */
40
+ children: React.ReactElement | ((linkProps: NavigationTabItemLinkProps) => React.ReactElement);
41
+ /**
42
+ * An id for the root element.
43
+ */
44
+ id?: string;
45
+ /**
46
+ * Optional test ID for e2e testing.
47
+ */
48
+ testId?: string;
49
+ /**
50
+ * If the `NavigationTabItem` is the current page. If `true`, current
51
+ * styling and aria-current=page will be applied to the Link.
52
+ *
53
+ * Note: NavigationTabs provides the styling for the current tab item.
54
+ */
55
+ current?: boolean;
56
+ /**
57
+ * Custom styles for overriding default styles. For custom link styling,
58
+ * prefer applying the styles to the `Link` component. Note: The
59
+ * `NavigationTabItem` will also set styles to the `Link` child component.
60
+ */
61
+ style?: StyleType;
62
+ } & React.RefAttributes<HTMLLIElement>>;
63
+ export {};
@@ -0,0 +1,72 @@
1
+ import { StyleType } from "@khanacademy/wonder-blocks-core";
2
+ import * as React from "react";
3
+ /**
4
+ * The `NavigationTabs` component is a tabbed interface for link navigation.
5
+ * The tabs are links and keyboard users can change tabs using tab.
6
+ * The `NavigationTabs` component is used with `NavigationTabItem` and `Link`
7
+ * components. If the tabs should not be links, see the `Tabs` component,
8
+ * which implements different semantics and keyboard interactions.
9
+ *
10
+ * ## Usage
11
+ *
12
+ * ```jsx
13
+ * import {NavigationTab, NavigationTabItem} from "@khanacademy/wonder-blocks-tabs";
14
+ * import Link from "@khanacademy/wonder-blocks-link";
15
+ *
16
+ * <NavigationTabs>
17
+ * <NavigationTabItem>
18
+ * <Link href="/link-1">Link 1</Link>
19
+ * </NavigationTabItem>
20
+ * <NavigationTabItem>
21
+ * <Link href="/link-2">Link 2</Link>
22
+ * </NavigationTabItem>
23
+ * </NavigationTabs>
24
+ * ```
25
+ */
26
+ export declare const NavigationTabs: React.ForwardRefExoticComponent<Readonly<import("@khanacademy/wonder-blocks-core").AriaAttributes> & Readonly<{
27
+ role?: import("@khanacademy/wonder-blocks-core").AriaRole;
28
+ }> & {
29
+ /**
30
+ * The NavigationTabItem components to render.
31
+ */
32
+ children: React.ReactElement | Array<React.ReactElement>;
33
+ /**
34
+ * An id for the navigation element.
35
+ */
36
+ id?: string;
37
+ /**
38
+ * Optional test ID for e2e testing.
39
+ */
40
+ testId?: string;
41
+ /**
42
+ * Accessible label for the navigation element.
43
+ *
44
+ * It is important to provide a unique aria-label if there are multiple
45
+ * navigation elements on the page.
46
+ *
47
+ * If there is a visual label for the navigation tabs already, use
48
+ * `aria-labelledby` instead.
49
+ */
50
+ "aria-label"?: string;
51
+ /**
52
+ * If there is a visual label for the navigation tabs already, set
53
+ * `aria-labelledby` to the `id` of the element that labels the navigation
54
+ * tabs.
55
+ */
56
+ "aria-labelledby"?: string;
57
+ /**
58
+ * Custom styles for the elements in NavigationTabs.
59
+ * - `root`: Styles the root `nav` element.
60
+ * - `list`: Styles the underlying `ul` element that wraps the
61
+ * `NavigationTabItem` components
62
+ */
63
+ styles?: {
64
+ root?: StyleType;
65
+ list?: StyleType;
66
+ };
67
+ /**
68
+ * Whether to include animation in the `NavigationTabs`. This should be false
69
+ * if the user has `prefers-reduced-motion` opted in. Defaults to `false`.
70
+ */
71
+ animated?: boolean;
72
+ } & React.RefAttributes<HTMLElement>>;
package/dist/es/index.js CHANGED
@@ -1,14 +1,15 @@
1
1
  import _extends from '@babel/runtime/helpers/extends';
2
2
  import _objectWithoutPropertiesLoose from '@babel/runtime/helpers/objectWithoutPropertiesLoose';
3
- import { addStyle } from '@khanacademy/wonder-blocks-core';
3
+ import { addStyle, useOnMountEffect, View } from '@khanacademy/wonder-blocks-core';
4
4
  import { sizing, semanticColor, breakpoint } from '@khanacademy/wonder-blocks-tokens';
5
5
  import { StyleSheet } from 'aphrodite';
6
6
  import * as React from 'react';
7
7
  import { styles as styles$2 } from '@khanacademy/wonder-blocks-typography';
8
8
 
9
- const _excluded$1 = ["id", "testId", "children", "aria-label", "aria-labelledby", "styles"];
9
+ const _excluded$1 = ["id", "testId", "children", "aria-label", "aria-labelledby", "styles", "animated"];
10
10
  const StyledNav = addStyle("nav");
11
11
  const StyledUl = addStyle("ul");
12
+ const StyledDiv = addStyle("div");
12
13
  const NavigationTabs = React.forwardRef(function NavigationTabs(props, ref) {
13
14
  const {
14
15
  id,
@@ -16,9 +17,56 @@ const NavigationTabs = React.forwardRef(function NavigationTabs(props, ref) {
16
17
  children,
17
18
  "aria-label": ariaLabel,
18
19
  "aria-labelledby": ariaLabelledBy,
19
- styles: stylesProp
20
+ styles: stylesProp,
21
+ animated = false
20
22
  } = props,
21
23
  otherProps = _objectWithoutPropertiesLoose(props, _excluded$1);
24
+ const listRef = React.useRef(null);
25
+ const indicatorIsReady = React.useRef(false);
26
+ const [underlineStyle, setUnderlineStyle] = React.useState({
27
+ left: 0,
28
+ width: 0
29
+ });
30
+ const updateUnderlineStyle = React.useCallback(() => {
31
+ if (!listRef.current) {
32
+ return;
33
+ }
34
+ const activeTab = Array.from(listRef.current.children).find(child => {
35
+ var _child$children$;
36
+ return (_child$children$ = child.children[0]) == null ? void 0 : _child$children$.ariaCurrent;
37
+ });
38
+ if (activeTab) {
39
+ const tabRect = activeTab.getBoundingClientRect();
40
+ const parentRect = listRef.current.getBoundingClientRect();
41
+ const zoomFactor = parentRect.width / listRef.current.offsetWidth;
42
+ const left = (tabRect.left - parentRect.left) / zoomFactor;
43
+ const width = tabRect.width / zoomFactor;
44
+ setUnderlineStyle({
45
+ left,
46
+ width
47
+ });
48
+ }
49
+ }, [setUnderlineStyle, listRef]);
50
+ useOnMountEffect(() => {
51
+ if (!listRef.current || !window.ResizeObserver) {
52
+ return;
53
+ }
54
+ const observer = new window.ResizeObserver(([entry]) => {
55
+ if (entry) {
56
+ updateUnderlineStyle();
57
+ if (!indicatorIsReady.current) {
58
+ indicatorIsReady.current = true;
59
+ }
60
+ }
61
+ });
62
+ observer.observe(listRef.current);
63
+ return () => {
64
+ observer.disconnect();
65
+ };
66
+ });
67
+ React.useEffect(() => {
68
+ updateUnderlineStyle();
69
+ }, [children, updateUnderlineStyle]);
22
70
  return React.createElement(StyledNav, _extends({
23
71
  id: id,
24
72
  "data-testid": testId,
@@ -26,14 +74,26 @@ const NavigationTabs = React.forwardRef(function NavigationTabs(props, ref) {
26
74
  "aria-labelledby": ariaLabelledBy,
27
75
  ref: ref,
28
76
  style: [styles$1.nav, stylesProp == null ? void 0 : stylesProp.root]
29
- }, otherProps), React.createElement(StyledUl, {
30
- style: [styles$1.list, stylesProp == null ? void 0 : stylesProp.list]
31
- }, children));
77
+ }, otherProps), React.createElement(StyledDiv, {
78
+ style: styles$1.contents
79
+ }, React.createElement(StyledUl, {
80
+ style: [styles$1.list, stylesProp == null ? void 0 : stylesProp.list],
81
+ ref: listRef
82
+ }, children), indicatorIsReady.current && React.createElement(View, {
83
+ style: [{
84
+ transform: `translateX(${underlineStyle.left}px)`,
85
+ width: `${underlineStyle.width}px`
86
+ }, styles$1.currentUnderline, animated && styles$1.underlineTransition],
87
+ role: "presentation"
88
+ })));
32
89
  });
33
90
  const styles$1 = StyleSheet.create({
34
91
  nav: {
35
92
  overflowX: "auto"
36
93
  },
94
+ contents: {
95
+ position: "relative"
96
+ },
37
97
  list: {
38
98
  paddingInline: sizing.size_050,
39
99
  paddingBlock: sizing.size_0,
@@ -41,6 +101,16 @@ const styles$1 = StyleSheet.create({
41
101
  display: "flex",
42
102
  gap: sizing.size_200,
43
103
  flexWrap: "nowrap"
104
+ },
105
+ currentUnderline: {
106
+ position: "absolute",
107
+ bottom: 0,
108
+ left: 0,
109
+ height: sizing.size_050,
110
+ backgroundColor: semanticColor.action.secondary.progressive.default.foreground
111
+ },
112
+ underlineTransition: {
113
+ transition: "transform 0.3s ease, width 0.3s ease"
44
114
  }
45
115
  });
46
116
 
@@ -90,9 +160,8 @@ const styles = StyleSheet.create({
90
160
  }
91
161
  },
92
162
  current: {
93
- boxShadow: `inset 0 -${sizing.size_050} 0 0 ${semanticColor.action.secondary.progressive.default.foreground}`,
94
163
  [":has(a:hover)"]: {
95
- boxShadow: `inset 0 -${sizing.size_050} 0 0 ${semanticColor.action.secondary.progressive.default.foreground}`
164
+ boxShadow: "none"
96
165
  }
97
166
  },
98
167
  currentLink: {
@@ -0,0 +1,2 @@
1
+ export { NavigationTabs } from "./components/navigation-tabs";
2
+ export { NavigationTabItem } from "./components/navigation-tab-item";
package/dist/index.js CHANGED
@@ -34,9 +34,10 @@ var _extends__default = /*#__PURE__*/_interopDefaultLegacy(_extends);
34
34
  var _objectWithoutPropertiesLoose__default = /*#__PURE__*/_interopDefaultLegacy(_objectWithoutPropertiesLoose);
35
35
  var React__namespace = /*#__PURE__*/_interopNamespace(React);
36
36
 
37
- const _excluded$1 = ["id", "testId", "children", "aria-label", "aria-labelledby", "styles"];
37
+ const _excluded$1 = ["id", "testId", "children", "aria-label", "aria-labelledby", "styles", "animated"];
38
38
  const StyledNav = wonderBlocksCore.addStyle("nav");
39
39
  const StyledUl = wonderBlocksCore.addStyle("ul");
40
+ const StyledDiv = wonderBlocksCore.addStyle("div");
40
41
  const NavigationTabs = React__namespace.forwardRef(function NavigationTabs(props, ref) {
41
42
  const {
42
43
  id,
@@ -44,9 +45,56 @@ const NavigationTabs = React__namespace.forwardRef(function NavigationTabs(props
44
45
  children,
45
46
  "aria-label": ariaLabel,
46
47
  "aria-labelledby": ariaLabelledBy,
47
- styles: stylesProp
48
+ styles: stylesProp,
49
+ animated = false
48
50
  } = props,
49
51
  otherProps = _objectWithoutPropertiesLoose__default["default"](props, _excluded$1);
52
+ const listRef = React__namespace.useRef(null);
53
+ const indicatorIsReady = React__namespace.useRef(false);
54
+ const [underlineStyle, setUnderlineStyle] = React__namespace.useState({
55
+ left: 0,
56
+ width: 0
57
+ });
58
+ const updateUnderlineStyle = React__namespace.useCallback(() => {
59
+ if (!listRef.current) {
60
+ return;
61
+ }
62
+ const activeTab = Array.from(listRef.current.children).find(child => {
63
+ var _child$children$;
64
+ return (_child$children$ = child.children[0]) == null ? void 0 : _child$children$.ariaCurrent;
65
+ });
66
+ if (activeTab) {
67
+ const tabRect = activeTab.getBoundingClientRect();
68
+ const parentRect = listRef.current.getBoundingClientRect();
69
+ const zoomFactor = parentRect.width / listRef.current.offsetWidth;
70
+ const left = (tabRect.left - parentRect.left) / zoomFactor;
71
+ const width = tabRect.width / zoomFactor;
72
+ setUnderlineStyle({
73
+ left,
74
+ width
75
+ });
76
+ }
77
+ }, [setUnderlineStyle, listRef]);
78
+ wonderBlocksCore.useOnMountEffect(() => {
79
+ if (!listRef.current || !window.ResizeObserver) {
80
+ return;
81
+ }
82
+ const observer = new window.ResizeObserver(([entry]) => {
83
+ if (entry) {
84
+ updateUnderlineStyle();
85
+ if (!indicatorIsReady.current) {
86
+ indicatorIsReady.current = true;
87
+ }
88
+ }
89
+ });
90
+ observer.observe(listRef.current);
91
+ return () => {
92
+ observer.disconnect();
93
+ };
94
+ });
95
+ React__namespace.useEffect(() => {
96
+ updateUnderlineStyle();
97
+ }, [children, updateUnderlineStyle]);
50
98
  return React__namespace.createElement(StyledNav, _extends__default["default"]({
51
99
  id: id,
52
100
  "data-testid": testId,
@@ -54,14 +102,26 @@ const NavigationTabs = React__namespace.forwardRef(function NavigationTabs(props
54
102
  "aria-labelledby": ariaLabelledBy,
55
103
  ref: ref,
56
104
  style: [styles$1.nav, stylesProp == null ? void 0 : stylesProp.root]
57
- }, otherProps), React__namespace.createElement(StyledUl, {
58
- style: [styles$1.list, stylesProp == null ? void 0 : stylesProp.list]
59
- }, children));
105
+ }, otherProps), React__namespace.createElement(StyledDiv, {
106
+ style: styles$1.contents
107
+ }, React__namespace.createElement(StyledUl, {
108
+ style: [styles$1.list, stylesProp == null ? void 0 : stylesProp.list],
109
+ ref: listRef
110
+ }, children), indicatorIsReady.current && React__namespace.createElement(wonderBlocksCore.View, {
111
+ style: [{
112
+ transform: `translateX(${underlineStyle.left}px)`,
113
+ width: `${underlineStyle.width}px`
114
+ }, styles$1.currentUnderline, animated && styles$1.underlineTransition],
115
+ role: "presentation"
116
+ })));
60
117
  });
61
118
  const styles$1 = aphrodite.StyleSheet.create({
62
119
  nav: {
63
120
  overflowX: "auto"
64
121
  },
122
+ contents: {
123
+ position: "relative"
124
+ },
65
125
  list: {
66
126
  paddingInline: wonderBlocksTokens.sizing.size_050,
67
127
  paddingBlock: wonderBlocksTokens.sizing.size_0,
@@ -69,6 +129,16 @@ const styles$1 = aphrodite.StyleSheet.create({
69
129
  display: "flex",
70
130
  gap: wonderBlocksTokens.sizing.size_200,
71
131
  flexWrap: "nowrap"
132
+ },
133
+ currentUnderline: {
134
+ position: "absolute",
135
+ bottom: 0,
136
+ left: 0,
137
+ height: wonderBlocksTokens.sizing.size_050,
138
+ backgroundColor: wonderBlocksTokens.semanticColor.action.secondary.progressive.default.foreground
139
+ },
140
+ underlineTransition: {
141
+ transition: "transform 0.3s ease, width 0.3s ease"
72
142
  }
73
143
  });
74
144
 
@@ -118,9 +188,8 @@ const styles = aphrodite.StyleSheet.create({
118
188
  }
119
189
  },
120
190
  current: {
121
- boxShadow: `inset 0 -${wonderBlocksTokens.sizing.size_050} 0 0 ${wonderBlocksTokens.semanticColor.action.secondary.progressive.default.foreground}`,
122
191
  [":has(a:hover)"]: {
123
- boxShadow: `inset 0 -${wonderBlocksTokens.sizing.size_050} 0 0 ${wonderBlocksTokens.semanticColor.action.secondary.progressive.default.foreground}`
192
+ boxShadow: "none"
124
193
  }
125
194
  },
126
195
  currentLink: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanacademy/wonder-blocks-tabs",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "design": "v1",
5
5
  "description": "Tabs are used to control what content is shown",
6
6
  "main": "dist/index.js",
@@ -13,9 +13,9 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "@babel/runtime": "^7.24.5",
16
- "@khanacademy/wonder-blocks-tokens": "5.1.1",
16
+ "@khanacademy/wonder-blocks-core": "12.2.1",
17
17
  "@khanacademy/wonder-blocks-typography": "3.1.3",
18
- "@khanacademy/wonder-blocks-core": "12.2.1"
18
+ "@khanacademy/wonder-blocks-tokens": "5.1.1"
19
19
  },
20
20
  "peerDependencies": {
21
21
  "aphrodite": "^1.2.5",