diff --git a/packages/canon/src/components/Toolbar/Toolbar.styles.css b/packages/canon/src/components/Toolbar/Toolbar.styles.css index 0c432b971c..edcb3b4b8d 100644 --- a/packages/canon/src/components/Toolbar/Toolbar.styles.css +++ b/packages/canon/src/components/Toolbar/Toolbar.styles.css @@ -14,12 +14,33 @@ * limitations under the License. */ +.canon-ToolbarRoot { + display: flex; + flex-direction: column; + gap: var(--canon-space-2); +} + +.canon-Toolbar { + display: flex; + flex-direction: row; + align-items: center; + background-color: var(--canon-bg-surface-1); + padding-inline: var(--canon-space-3); + border-radius: var(--canon-radius-2); + border: 1px solid var(--canon-border); + color: var(--canon-fg-primary); + height: 52px; +} + .canon-ToolbarTabs { display: flex; flex-direction: row; align-items: center; background-color: var(--canon-bg-surface-1); position: relative; + padding-inline: var(--canon-space-1); + border-radius: var(--canon-radius-2); + border: 1px solid var(--canon-border); /* Initialize CSS variables */ --active-tab-left: 0px; @@ -35,6 +56,8 @@ --hovered-tab-bottom: 0px; --hovered-tab-width: 0px; --hovered-tab-height: 0px; + --hovered-tab-opacity: 0; + --hovered-transition-duration: 0s; } .canon-ToolbarTabList { @@ -52,17 +75,18 @@ cursor: pointer; position: relative; z-index: 2; + padding-inline: var(--canon-space-2); } /* Active tab indicator */ .canon-ToolbarActiveIndicator { content: ''; position: absolute; - left: var(--active-tab-left); - top: var(--active-tab-top); - width: var(--active-tab-width); - height: var(--active-tab-height); - background-color: green; + left: calc(var(--active-tab-left) + var(--canon-space-2)); + bottom: -1px; + width: calc(var(--active-tab-width) - var(--canon-space-4)); + height: 1px; + background-color: var(--canon-fg-primary); border-radius: 4px; transition: all 0.25s ease-out; opacity: 1; @@ -73,10 +97,14 @@ content: ''; position: absolute; left: var(--hovered-tab-left); - top: var(--hovered-tab-top); + top: calc(var(--hovered-tab-top) + 4px); width: var(--hovered-tab-width); - height: var(--hovered-tab-height); - background-color: red; + height: calc(var(--hovered-tab-height) - 8px); + background-color: var(--canon-gray-2); border-radius: 4px; - transition: all 0.2s ease-out; + opacity: var(--hovered-tab-opacity); + transition: left var(--hovered-transition-duration) ease-out, + top var(--hovered-transition-duration) ease-out, + width var(--hovered-transition-duration) ease-out, + height var(--hovered-transition-duration) ease-out, opacity 0.15s ease-out; } diff --git a/packages/canon/src/components/Toolbar/Toolbar.tsx b/packages/canon/src/components/Toolbar/Toolbar.tsx index d6f5b4d7f3..65ee408399 100644 --- a/packages/canon/src/components/Toolbar/Toolbar.tsx +++ b/packages/canon/src/components/Toolbar/Toolbar.tsx @@ -28,9 +28,12 @@ import { useEffect, useState, useCallback, - MutableRefObject, } from 'react'; -import { ToolbarProps, ToolbarTab } from './types'; +import type { + ToolbarProps, + ToolbarIndicatorsProps, + ToolbarTabProps, +} from './types'; export const Toolbar = (props: ToolbarProps) => { const { tabs } = props; @@ -38,6 +41,7 @@ export const Toolbar = (props: ToolbarProps) => { const tabsRef = useRef(null); const tabRefs = useRef>(new Map()); const [hoveredKey, setHoveredKey] = useState(null); + const prevHoveredKey = useRef(null); const setTabRef = (key: string, element: HTMLDivElement | null) => { if (element) { @@ -48,8 +52,8 @@ export const Toolbar = (props: ToolbarProps) => { }; return ( - <> -
Toolbar
+
+
Toolbar
{tabs && ( @@ -64,26 +68,20 @@ export const Toolbar = (props: ToolbarProps) => { ); })} - )} - +
); }; -const Tab = ({ - tab, - setTabRef, - setHoveredKey, -}: { - tab: ToolbarTab; - setTabRef: (key: string, element: HTMLDivElement | null) => void; - setHoveredKey: (key: string | null) => void; -}) => { +const Tab = (props: ToolbarTabProps) => { + const { tab, setTabRef, setHoveredKey } = props; const id = useId(); const { classNames } = useStyles('Toolbar'); @@ -100,15 +98,8 @@ const Tab = ({ ); }; -const Indicator = ({ - tabRefs, - tabsRef, - hoveredKey, -}: { - tabRefs: MutableRefObject>; - tabsRef: MutableRefObject; - hoveredKey: string | null; -}) => { +const Indicators = (props: ToolbarIndicatorsProps) => { + const { tabRefs, tabsRef, hoveredKey, prevHoveredKey } = props; const { classNames } = useStyles('Toolbar'); const state = useContext(TabListStateContext); @@ -120,7 +111,7 @@ const Indicator = ({ // Set active tab variables if (state?.selectedKey) { const activeTab = tabRefs.current.get(state.selectedKey.toString()); - console.log(activeTab); + if (activeTab) { const activeRect = activeTab.getBoundingClientRect(); const relativeLeft = activeRect.left - tabsRect.left; @@ -185,15 +176,43 @@ const Indicator = ({ '--hovered-tab-height', `${hoveredRect.height}px`, ); + // Control transition timing based on whether this is a new hover session + const isNewHoverSession = prevHoveredKey.current === null; + + if (isNewHoverSession) { + // Starting new hover session: no transitions for position + tabsRef.current.style.setProperty( + '--hovered-transition-duration', + '0s', + ); + // Enable transitions on next frame for future tab switches + requestAnimationFrame(() => { + if (tabsRef.current) { + tabsRef.current.style.setProperty( + '--hovered-transition-duration', + '0.2s', + ); + } + }); + } else { + // Moving between tabs in same session: full transitions + tabsRef.current.style.setProperty( + '--hovered-transition-duration', + '0.2s', + ); + } + + // Update previous hover key for next time + prevHoveredKey.current = hoveredKey; + + tabsRef.current.style.setProperty('--hovered-tab-opacity', '1'); } } else { - // Clear hovered variables when no tab is hovered - tabsRef.current.style.setProperty('--hovered-tab-left', '0px'); - tabsRef.current.style.setProperty('--hovered-tab-right', '0px'); - tabsRef.current.style.setProperty('--hovered-tab-top', '0px'); - tabsRef.current.style.setProperty('--hovered-tab-bottom', '0px'); - tabsRef.current.style.setProperty('--hovered-tab-width', '0px'); - tabsRef.current.style.setProperty('--hovered-tab-height', '0px'); + // When not hovering, hide with opacity and reset for next hover session + tabsRef.current.style.setProperty('--hovered-tab-opacity', '0'); + + // Reset previous hover key so next hover is treated as new session + prevHoveredKey.current = null; } }, [state?.selectedKey, hoveredKey]); diff --git a/packages/canon/src/components/Toolbar/types.ts b/packages/canon/src/components/Toolbar/types.ts index 48f5d53c10..572a1beee7 100644 --- a/packages/canon/src/components/Toolbar/types.ts +++ b/packages/canon/src/components/Toolbar/types.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { MutableRefObject } from 'react'; + export interface ToolbarTab { label: string; } @@ -21,3 +23,16 @@ export interface ToolbarTab { export interface ToolbarProps { tabs: ToolbarTab[]; } + +export interface ToolbarTabProps { + tab: ToolbarTab; + setTabRef: (key: string, element: HTMLDivElement | null) => void; + setHoveredKey: (key: string | null) => void; +} + +export interface ToolbarIndicatorsProps { + tabRefs: MutableRefObject>; + tabsRef: MutableRefObject; + hoveredKey: string | null; + prevHoveredKey: MutableRefObject; +} diff --git a/packages/canon/src/utils/componentDefinitions.ts b/packages/canon/src/utils/componentDefinitions.ts index b23757313d..ee3e9775d2 100644 --- a/packages/canon/src/utils/componentDefinitions.ts +++ b/packages/canon/src/utils/componentDefinitions.ts @@ -237,6 +237,8 @@ export const componentDefinitions = { }, Toolbar: { classNames: { + root: 'canon-ToolbarRoot', + toolbar: 'canon-Toolbar', tabs: 'canon-ToolbarTabs', tabList: 'canon-ToolbarTabList', tab: 'canon-ToolbarTab',