Skip to content

feat: Add Toolbar component to teams-components #311

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
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
@@ -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.54.13",
"@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",
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { makeStyles } from '@fluentui/react-components';

export const useStyles = makeStyles({
root: {
display: 'flex',
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as React from 'react';
import { render } from '@testing-library/react';
import { Toolbar } from './Toolbar';

describe('Toolbar', () => {
it('should render', () => {
render(<Toolbar />);
});
});
87 changes: 87 additions & 0 deletions packages/teams-components/src/components/Toolbar/Toolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import {
mergeClasses,
useArrowNavigationGroup,
} from '@fluentui/react-components';
import * as React from 'react';
import { useStyles } from './Toolbar.styles';
import { StrictCssClass } from '../../strictStyles';
import {
ToolbarItemRegistrationProvider,
useInitItemRegistration,
} from './itemRegistration';

export interface ToolbarProps {
children: React.ReactNode;
className?: StrictCssClass;
}

export const toolbarClassNames = {
root: 'tco-Toolbar',
};

export const Toolbar = React.forwardRef<HTMLDivElement, ToolbarProps>(
(props, ref) => {
const { children, className } = props;
const styles = useStyles();
const registerItem = useInitItemRegistration();
const contextValue = React.useMemo(
() => ({ registerItem }),
[registerItem]
);

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

// 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
@@ -0,0 +1,33 @@
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',
};

export type ToolbarButtonProps = Omit<ButtonProps, 'className'>;

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={useMergedRefs(ref, registerRef)}
{...props}
className={mergeStrictClasses(rootStrictClassName, styles.root)}
data-appearance={props.appearance}
/>
);
}
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { makeStyles } from '@fluentui/react-components';

export const useStyles = makeStyles({
root: {
flexGrow: 'unset',
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as React from 'react';
import {
useDividerStyles_unstable,
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',
};

export const ToolbarDivider = React.forwardRef<
HTMLDivElement,
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,
itemRegistrationStyles.root.toString()
),
},
useMergedRefs(ref, registerRef)
);
useDividerStyles_unstable(state);
Copy link
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


return renderDivider_unstable(state);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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'
>;

const rootStrictClassName = createStrictClass(toolbarMenuButtonClassNames.root);

// TODO teams-components should reuse composition patterns
export const ToolbarMenuButton = React.forwardRef<
HTMLButtonElement,
MenuButtonProps
>((props, ref) => {
const { ref: registerRef, styles } = useItemRegistration({
appearance: props.appearance,
type: 'button',
});
return (
<MenuButton
ref={useMergedRefs(ref, registerRef)}
{...props}
menuIcon={null}
className={mergeStrictClasses(rootStrictClassName, styles.root)}
data-appearance={props.appearance}
/>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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',
};

export type ToolbarToggleButtonProps = Omit<ToggleButtonProps, 'className'>;

const rootStrictClassName = createStrictClass(
toolbarToggleButtonClassNames.root
);

// TODO teams-components should reuse composition patterns
export const ToolbarToggleButton = React.forwardRef<
HTMLButtonElement,
ToggleButtonProps
>((props, ref) => {
const { ref: registerRef, styles } = useItemRegistration({
appearance: props.appearance,
type: 'button',
});
return (
<ToggleButton
ref={useMergedRefs(ref, registerRef)}
{...props}
className={mergeStrictClasses(rootStrictClassName, styles.root)}
data-appearance={props.appearance}
/>
);
});
5 changes: 5 additions & 0 deletions packages/teams-components/src/components/Toolbar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './Toolbar';
export * from './ToolbarDivider';
export * from './ToolbarButton';
export * from './ToolbarToggleButton';
export * from './ToolbarMenuButton';
Comment on lines +1 to +5
Copy link
Member

Choose a reason for hiding this comment

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

This is too wild, let's be explicit

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
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
Oops, something went wrong.
Loading
Oops, something went wrong.