Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"@dnd-kit/modifiers": "^7.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@fluentui/react": "^8.120.2",
"@fluentui/react-components": "^9.58.3",
"@fluentui/react-components": "^9.59.0",
"@fluentui/react-icons": "^2.0.249",
"@fluentui/react-migration-v8-v9": "^9.6.23",
"@fluentui/react-shared-contexts": "^9.7.2",
Expand Down
201 changes: 60 additions & 141 deletions packages/teams-components/src/components/Toolbar/Toolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import {
tokens,
useMergedRefs,
mergeClasses,
useArrowNavigationGroup,
} from '@fluentui/react-components';
import { isHTMLElement } from '@fluentui/react-utilities';
import * as React from 'react';
import { useStyles } from './Toolbar.styles';
import { HTMLElementWalker } from '../../elementWalker';
import { StrictCssClass } from '../../strictStyles';
import { toolbarButtonClassNames } from './ToolbarButton';
import { toolbarDividerClassNames } from './ToolbarDivider';
import { toolbarToggleButtonClassNames } from './ToolbarToggleButton';
import { toolbarMenuButtonClassNames } from './ToolbarMenuButton';
import {
ToolbarItemRegistrationProvider,
useInitItemRegistration,
} from './itemRegistration';

export interface ToolbarProps {
children: React.ReactNode;
Expand All @@ -27,142 +23,65 @@ export const Toolbar = React.forwardRef<HTMLDivElement, ToolbarProps>(
(props, ref) => {
const { children, className } = props;
const styles = useStyles();
const enforceSpacingRef = useEnforceItemSpacing();
const registerItem = useInitItemRegistration();
const contextValue = React.useMemo(
() => ({ registerItem }),
[registerItem]
);

return (
<div
role="toolbar"
className={mergeClasses(
toolbarClassNames.root,
styles.root,
className?.toString()
)}
ref={useMergedRefs(ref, enforceSpacingRef)}
{...useArrowNavigationGroup({ axis: 'both', circular: true })}
>
{children}
</div>
<ToolbarItemRegistrationProvider value={contextValue}>
<div
role="toolbar"
className={mergeClasses(
toolbarClassNames.root,
styles.root,
className?.toString()
)}
ref={ref}
{...useArrowNavigationGroup({ axis: 'both', circular: true })}
>
{children}
</div>
</ToolbarItemRegistrationProvider>
);
}
);

const useEnforceItemSpacing = () => {
const elRef = React.useRef<HTMLDivElement | null>(null);

React.useLayoutEffect(() => {
if (!elRef.current?.ownerDocument.defaultView) {
return;
}

if (process.env.NODE_ENV !== 'production') {
validateToolbarItems(elRef.current);
}

const treeWalker = new HTMLElementWalker(elRef.current, (el) => {
if (isAllowedToolbarItem(el) || el === treeWalker.root) {
return NodeFilter.FILTER_ACCEPT;
}

return NodeFilter.FILTER_REJECT;
});

reaclcToolbarSpacing(treeWalker);

const mutationObserver =
new elRef.current.ownerDocument.defaultView.MutationObserver(() => {
if (!elRef.current) {
return;
}

if (process.env.NODE_ENV !== 'production') {
validateToolbarItems(elRef.current);
}

// TODO can optimize by only doing recalc of affected elements
reaclcToolbarSpacing(treeWalker);
});

mutationObserver.observe(elRef.current, {
childList: true,
});

return () => mutationObserver.disconnect();
}, []);

return elRef;
};

const reaclcToolbarSpacing = (treeWalker: HTMLElementWalker) => {
treeWalker.currentElement = treeWalker.root;
let current = treeWalker.firstChild();
while (current) {
recalcToolbarItemSpacing(current, treeWalker);

treeWalker.currentElement = current;
current = treeWalker.nextElement();
}
};

const isAllowedToolbarItem = (el: HTMLElement) => {
return (
el.classList.contains(toolbarButtonClassNames.root) ||
el.classList.contains(toolbarDividerClassNames.root) ||
el.classList.contains(toolbarMenuButtonClassNames.root) ||
el.classList.contains(toolbarToggleButtonClassNames.root)
);
};

const isPortalSpan = (el: HTMLElement) => {
return el.tagName === 'SPAN' && el.hasAttribute('hidden');
};

const isTabsterDummy = (el: HTMLElement) => {
return el.hasAttribute('data-tabster-dummy');
};

const validateToolbarItems = (root: HTMLElement) => {
const children = root.children;
for (const child of children) {
// TODO is this even possible?
if (!isHTMLElement(child)) {
continue;
}

if (
!isAllowedToolbarItem(child) &&
!isPortalSpan(child) &&
!isTabsterDummy(child)
) {
throw new Error(
'@fluentui-contrib/teams-components::Toolbar::Use Toolbar components from @fluentui-contrib/teams-components package only'
);
}
}
};

const recalcToolbarItemSpacing = (
el: HTMLElement,
treeWalker: HTMLElementWalker
) => {
treeWalker.currentElement = treeWalker.root;
if (el === treeWalker.firstChild() || !isAllowedToolbarItem(el)) {
return;
}

if (el.classList.contains(toolbarDividerClassNames.root)) {
el.style.marginInlineStart = tokens.spacingHorizontalS;
return;
}

treeWalker.currentElement = el;
const prev = treeWalker.previousElement();
if (prev && prev.dataset.appearance !== 'transparent') {
el.style.marginInlineStart = tokens.spacingHorizontalS;
return;
}

if (prev && el.dataset.appearance !== 'transparent') {
prev.style.marginInlineStart = tokens.spacingHorizontalS;
return;
}
};
// TODO implement DOM validation API
// const isAllowedToolbarItem = (el: HTMLElement) => {
// return (
// el.classList.contains(toolbarButtonClassNames.root) ||
// el.classList.contains(toolbarDividerClassNames.root) ||
// el.classList.contains(toolbarMenuButtonClassNames.root) ||
// el.classList.contains(toolbarToggleButtonClassNames.root)
// );
// };
//
// const isPortalSpan = (el: HTMLElement) => {
// return el.tagName === 'SPAN' && el.hasAttribute('hidden');
// };
//
// const isTabsterDummy = (el: HTMLElement) => {
// return el.hasAttribute('data-tabster-dummy');
// };
//
// const validateToolbarItems = (root: HTMLElement) => {
// const children = root.children;
// for (const child of children) {
// // TODO is this even possible?
// if (!isHTMLElement(child)) {
// continue;
// }
//
// if (
// !isAllowedToolbarItem(child) &&
// !isPortalSpan(child) &&
// !isTabsterDummy(child)
// ) {
// throw new Error(
// '@fluentui-contrib/teams-components::Toolbar::Use Toolbar components from @fluentui-contrib/teams-components package only'
// );
// }
// }
// };
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import * as React from 'react';
import { Button, type ButtonProps } from '../Button';
import { createStrictClass } from '../../strictStyles/createStrictClass';
import { useItemRegistration } from './itemRegistration';
import { useMergedRefs } from '@fluentui/react-components';
import { mergeStrictClasses } from '../../strictStyles/mergeStrictClasses';

export const toolbarButtonClassNames = {
root: 'tco-ToolbarButton',
Expand All @@ -13,11 +16,16 @@ const rootStrictClassName = createStrictClass(toolbarButtonClassNames.root);
// TODO teams-components should reuse composition patterns
export const ToolbarButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
(props, ref) => {
const { ref: registerRef, styles } = useItemRegistration({
appearance: props.appearance,
type: 'button',
});

return (
<Button
ref={ref}
ref={useMergedRefs(ref, registerRef)}
{...props}
className={rootStrictClassName}
className={mergeStrictClasses(rootStrictClassName, styles.root)}
data-appearance={props.appearance}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import {
useDivider_unstable,
renderDivider_unstable,
mergeClasses,
useMergedRefs,
} from '@fluentui/react-components';
import { useStyles } from './ToolbarDivider.styles';
import { useItemRegistration } from './itemRegistration';

export const toolbarDividerClassNames = {
root: 'tco-ToolbarDivider',
Expand All @@ -16,13 +18,22 @@ export const ToolbarDivider = React.forwardRef<
Record<string, never>
>((props, ref) => {
const styles = useStyles();
const { ref: registerRef, styles: itemRegistrationStyles } =
useItemRegistration({
appearance: props.appearance,
type: 'divider',
});
const state = useDivider_unstable(
{
...props,
vertical: true,
className: mergeClasses(toolbarDividerClassNames.root, styles.root),
className: mergeClasses(
toolbarDividerClassNames.root,
styles.root,
itemRegistrationStyles.root.toString()
),
},
ref
useMergedRefs(ref, registerRef)
);
useDividerStyles_unstable(state);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would double check if we really want to reuse Divider there


Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import * as React from 'react';
import { MenuButton, MenuButtonProps } from '../MenuButton';
import { createStrictClass } from '../../strictStyles/createStrictClass';
import { useItemRegistration } from './itemRegistration';
import { useMergedRefs } from '@fluentui/react-components';
import { mergeStrictClasses } from '../../strictStyles';

export const toolbarMenuButtonClassNames = {
root: 'tco-ToolbarMenuButton',
};

export type ToolbarMenuButtonProps = Omit<MenuButtonProps, 'className' | 'menuIcon'>;
export type ToolbarMenuButtonProps = Omit<
MenuButtonProps,
'className' | 'menuIcon'
>;

const rootStrictClassName = createStrictClass(toolbarMenuButtonClassNames.root);

Expand All @@ -15,12 +21,16 @@ export const ToolbarMenuButton = React.forwardRef<
HTMLButtonElement,
MenuButtonProps
>((props, ref) => {
const { ref: registerRef, styles } = useItemRegistration({
appearance: props.appearance,
type: 'button',
});
return (
<MenuButton
ref={ref}
ref={useMergedRefs(ref, registerRef)}
{...props}
menuIcon={null}
className={rootStrictClassName}
className={mergeStrictClasses(rootStrictClassName, styles.root)}
data-appearance={props.appearance}
/>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import * as React from 'react';
import { ToggleButton, type ToggleButtonProps } from '../ToggleButton';
import { createStrictClass } from '../../strictStyles/createStrictClass';
import { useItemRegistration } from './itemRegistration';
import { useMergedRefs } from '@fluentui/react-components';
import { mergeStrictClasses } from '../../strictStyles';

export const toolbarToggleButtonClassNames = {
root: 'tco-ToolbarToggleButton',
Expand All @@ -17,11 +20,15 @@ export const ToolbarToggleButton = React.forwardRef<
HTMLButtonElement,
ToggleButtonProps
>((props, ref) => {
const { ref: registerRef, styles } = useItemRegistration({
appearance: props.appearance,
type: 'button',
});
return (
<ToggleButton
ref={ref}
ref={useMergedRefs(ref, registerRef)}
{...props}
className={rootStrictClassName}
className={mergeStrictClasses(rootStrictClassName, styles.root)}
data-appearance={props.appearance}
/>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { makeStrictStyles } from '../../strictStyles/makeStrictStyles';

export const itemRegistrationVars = {
toolbarItemMarginInlineStart: '--toolbar-item-margin-inline-start',
};

let propertyRegisterComplete = false;

export const registerCustomProperties = (win: typeof globalThis) => {
if (propertyRegisterComplete) {
Comment on lines +7 to +10
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be done per window?

return;
}

try {
win.CSS.registerProperty({
name: itemRegistrationVars.toolbarItemMarginInlineStart,
syntax: '<length>',
inherits: false,
initialValue: '0px',
});
} catch {
// ignore multiple registration error
}

propertyRegisterComplete = true;
};

export const useItemRegistrationStyles = makeStrictStyles({
root: {
marginInlineStart: `var(${itemRegistrationVars.toolbarItemMarginInlineStart})`,
},
});
Loading