Skip to content

Commit 1290251

Browse files
committed
Add <IconButton> middleware
1 parent 91b52c3 commit 1290251

13 files changed

Lines changed: 246 additions & 30 deletions

package-lock.json

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/api-middleware/src/PolymiddlewareComposer.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
extractChatLauncherButtonEnhancer
2121
} from './chatLauncherButtonPolymiddleware';
2222
import { ErrorBoxPolymiddlewareProvider, extractErrorBoxEnhancer } from './errorBoxPolymiddleware';
23+
import { IconButtonPolymiddlewareProvider, extractIconButtonEnhancer } from './iconButtonPolymiddleware';
2324
import { Polymiddleware } from './types/Polymiddleware';
2425

2526
const polymiddlewareComposerPropsSchema = pipe(
@@ -88,6 +89,24 @@ function PolymiddlewareComposer(props: PolymiddlewareComposerProps) {
8889

8990
const errorBoxPolymiddleware = useMemo(() => errorBoxEnhancers.map(enhancer => () => enhancer), [errorBoxEnhancers]);
9091

92+
const iconButtonEnhancers = useMemoWithPrevious<ReturnType<typeof extractIconButtonEnhancer>>(
93+
(prevIconButtonEnhancers = []) => {
94+
const iconButtonEnhancers = extractIconButtonEnhancer(polymiddleware);
95+
96+
// Checks for array equality, return previous version if nothing has changed.
97+
return prevIconButtonEnhancers.length === iconButtonEnhancers.length &&
98+
iconButtonEnhancers.every((middleware, index) => Object.is(middleware, prevIconButtonEnhancers.at(index)))
99+
? prevIconButtonEnhancers
100+
: iconButtonEnhancers;
101+
},
102+
[polymiddleware]
103+
);
104+
105+
const iconButtonPolymiddleware = useMemo(
106+
() => iconButtonEnhancers.map(enhancer => () => enhancer),
107+
[iconButtonEnhancers]
108+
);
109+
91110
// Didn't thoroughly think through this part yet, but I am using the first approach for now:
92111

93112
// 1. <XXXProvider> for every type of middleware
@@ -100,7 +119,11 @@ function PolymiddlewareComposer(props: PolymiddlewareComposerProps) {
100119
return (
101120
<ActivityPolymiddlewareProvider middleware={activityPolymiddleware}>
102121
<ChatLauncherButtonPolymiddlewareProvider middleware={chatLauncherButtonPolymiddleware}>
103-
<ErrorBoxPolymiddlewareProvider middleware={errorBoxPolymiddleware}>{children}</ErrorBoxPolymiddlewareProvider>
122+
<ErrorBoxPolymiddlewareProvider middleware={errorBoxPolymiddleware}>
123+
<IconButtonPolymiddlewareProvider middleware={iconButtonPolymiddleware}>
124+
{children}
125+
</IconButtonPolymiddlewareProvider>
126+
</ErrorBoxPolymiddlewareProvider>
104127
</ChatLauncherButtonPolymiddlewareProvider>
105128
</ActivityPolymiddlewareProvider>
106129
);

packages/api-middleware/src/chatLauncherButtonPolymiddleware.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const {
2222
reactComponent: chatLauncherButtonComponent,
2323
useBuildRenderCallback: useBuildRenderChatLauncherButtonCallback
2424
} = templatePolymiddleware<EmptyObject, { readonly children?: never; readonly hasMessage: boolean }>(
25-
'chatLauncherButton'
25+
'ChatLauncherButton'
2626
);
2727

2828
type ChatLauncherButtonPolymiddleware = InferMiddleware<typeof Provider>;
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { reactNode, validateProps } from '@msinternal/botframework-webchat-react-valibot';
2+
import React, { memo, useMemo, type ReactNode } from 'react';
3+
import { type EmptyObject } from 'type-fest';
4+
import { function_, object, optional, pipe, readonly, string, type InferInput } from 'valibot';
5+
6+
import createErrorBoundaryMiddleware from './private/createErrorBoundaryMiddleware';
7+
import templatePolymiddleware, {
8+
type InferHandler,
9+
type InferHandlerResult,
10+
type InferMiddleware,
11+
type InferProps,
12+
type InferProviderProps,
13+
type InferRenderer,
14+
type InferRequest
15+
} from './private/templatePolymiddleware';
16+
17+
const {
18+
createMiddleware: createIconButtonPolymiddleware,
19+
extractEnhancer: extractIconButtonEnhancer,
20+
Provider,
21+
Proxy,
22+
reactComponent: iconButtonComponent,
23+
useBuildRenderCallback: useBuildRenderIconButtonCallback
24+
} = templatePolymiddleware<
25+
EmptyObject,
26+
{
27+
readonly children?: ReactNode | undefined;
28+
readonly className?: string | undefined;
29+
readonly onClick?: (() => void) | undefined;
30+
}
31+
>('IconButton');
32+
33+
type IconButtonPolymiddleware = InferMiddleware<typeof Provider>;
34+
type IconButtonPolymiddlewareHandler = InferHandler<typeof Provider>;
35+
type IconButtonPolymiddlewareHandlerResult = InferHandlerResult<typeof Provider>;
36+
type IconButtonPolymiddlewareProps = InferProps<typeof Provider>;
37+
type IconButtonPolymiddlewareRenderer = InferRenderer<typeof Provider>;
38+
type IconButtonPolymiddlewareRequest = InferRequest<typeof Provider>;
39+
type IconButtonPolymiddlewareProviderProps = InferProviderProps<typeof Provider>;
40+
41+
const iconButtonPolymiddlewareProxyPropsSchema = pipe(
42+
object({
43+
children: optional(reactNode()),
44+
className: optional(string()),
45+
onClick: optional(function_())
46+
}),
47+
readonly()
48+
);
49+
50+
type IconButtonPolymiddlewareProxyProps = Readonly<InferInput<typeof iconButtonPolymiddlewareProxyPropsSchema>>;
51+
52+
const EMPTY_OBJECT = Object.freeze({});
53+
54+
// A friendlier version than the organic <Proxy>.
55+
const IconButtonPolymiddlewareProxy = memo(function IconButtonPolymiddlewareProxy(
56+
props: IconButtonPolymiddlewareProxyProps
57+
) {
58+
const { children, className, onClick } = validateProps(iconButtonPolymiddlewareProxyPropsSchema, props);
59+
60+
return (
61+
<Proxy className={className} onClick={onClick} request={EMPTY_OBJECT}>
62+
{children}
63+
</Proxy>
64+
);
65+
});
66+
67+
const IconButtonPolymiddlewareProvider = memo(function IconButtonPolymiddlewareProvider({
68+
children,
69+
middleware
70+
}: IconButtonPolymiddlewareProviderProps) {
71+
// Decorates middleware with <ErrorBoundary>.
72+
const middlewareWithErrorBoundary = useMemo(
73+
() =>
74+
Object.freeze([
75+
// TODO: [P1] Should we simplify this middleware signature and allow error boundary middleware to run on every type of middleware?
76+
// (init: any) => next => (request: undefined) => reactComponentForAll()
77+
// We can't do it today because we have sanity check that prevent `reactComponent()` from different middleware cross-polluting each other.
78+
createErrorBoundaryMiddleware({
79+
createMiddleware: createIconButtonPolymiddleware,
80+
reactComponent: iconButtonComponent,
81+
where: 'Icon button polymiddleware'
82+
}),
83+
...middleware
84+
]),
85+
[middleware]
86+
);
87+
88+
return <Provider middleware={middlewareWithErrorBoundary}>{children}</Provider>;
89+
});
90+
91+
export {
92+
createIconButtonPolymiddleware,
93+
extractIconButtonEnhancer,
94+
iconButtonComponent,
95+
IconButtonPolymiddlewareProvider,
96+
IconButtonPolymiddlewareProxy,
97+
useBuildRenderIconButtonCallback,
98+
type IconButtonPolymiddleware,
99+
type IconButtonPolymiddlewareHandler,
100+
type IconButtonPolymiddlewareHandlerResult,
101+
type IconButtonPolymiddlewareProps,
102+
type IconButtonPolymiddlewareProviderProps,
103+
type IconButtonPolymiddlewareProxyProps,
104+
type IconButtonPolymiddlewareRenderer,
105+
type IconButtonPolymiddlewareRequest
106+
};

packages/api-middleware/src/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,20 @@ export {
4040
type ErrorBoxPolymiddlewareRequest
4141
} from './errorBoxPolymiddleware';
4242

43+
export {
44+
createIconButtonPolymiddleware,
45+
iconButtonComponent,
46+
IconButtonPolymiddlewareProxy,
47+
useBuildRenderIconButtonCallback,
48+
type IconButtonPolymiddleware,
49+
type IconButtonPolymiddlewareHandler,
50+
type IconButtonPolymiddlewareHandlerResult,
51+
type IconButtonPolymiddlewareProps,
52+
type IconButtonPolymiddlewareProxyProps,
53+
type IconButtonPolymiddlewareRenderer,
54+
type IconButtonPolymiddlewareRequest
55+
} from './iconButtonPolymiddleware';
56+
4357
// TODO: [P0] Add tests for nesting `polymiddleware`.
4458
export { default as PolymiddlewareComposer } from './PolymiddlewareComposer';
4559
export { type Polymiddleware } from './types/Polymiddleware';
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { type ActivityPolymiddleware } from '../activityPolymiddleware';
22
import { type ChatLauncherButtonPolymiddleware } from '../chatLauncherButtonPolymiddleware';
33
import { type ErrorBoxPolymiddleware } from '../errorBoxPolymiddleware';
4+
import { type IconButtonPolymiddleware } from '../iconButtonPolymiddleware';
45

5-
export type Polymiddleware = ActivityPolymiddleware | ChatLauncherButtonPolymiddleware | ErrorBoxPolymiddleware;
6+
export type Polymiddleware =
7+
| ActivityPolymiddleware
8+
| ChatLauncherButtonPolymiddleware
9+
| ErrorBoxPolymiddleware
10+
| IconButtonPolymiddleware;

packages/api/src/boot/middleware.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,18 @@ export {
4444
type ErrorBoxPolymiddlewareRequest
4545
} from '@msinternal/botframework-webchat-api-middleware';
4646

47+
export {
48+
createIconButtonPolymiddleware,
49+
iconButtonComponent,
50+
IconButtonPolymiddlewareProxy,
51+
useBuildRenderIconButtonCallback,
52+
type IconButtonPolymiddleware,
53+
type IconButtonPolymiddlewareHandler,
54+
type IconButtonPolymiddlewareHandlerResult,
55+
type IconButtonPolymiddlewareProps,
56+
type IconButtonPolymiddlewareProxyProps,
57+
type IconButtonPolymiddlewareRenderer,
58+
type IconButtonPolymiddlewareRequest
59+
} from '@msinternal/botframework-webchat-api-middleware';
60+
4761
export { default as createActivityPolymiddlewareFromLegacy } from '../legacy/createActivityPolymiddlewareFromLegacy';

packages/experience-chat-launcher/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"botframework-webchat-api": "0.0.0-0",
6565
"botframework-webchat-component": "0.0.0-0",
6666
"botframework-webchat-core": "0.0.0-0",
67+
"use-ref-from": "0.1.0",
6768
"valibot": "1.1.0"
6869
}
6970
}

packages/experience-chat-launcher/src/private/ChatLauncher.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {
55
chatLauncherButtonComponent,
66
ChatLauncherButtonPolymiddlewareProxy,
77
createChatLauncherButtonPolymiddleware,
8+
createIconButtonPolymiddleware,
9+
iconButtonComponent,
810
type Polymiddleware
911
} from 'botframework-webchat-api/middleware';
1012
import { Composer } from 'botframework-webchat-component/component';
@@ -27,6 +29,7 @@ import {
2729
import ChatLauncherStylesheet from '../stylesheet/ChatLauncherStylesheet';
2830
import styles from './ChatLauncher.module.css';
2931
import ChatLauncherButton from './private/ChatLauncherButton';
32+
import IconButton from './private/IconButton';
3033

3134
// Best-effort to check if it is an Observable.
3235
const observableSchema = object({ subscribe: function_() });
@@ -63,7 +66,8 @@ function ChatLauncher(props: ChatLauncherProps) {
6366
const polymiddleware = useMemo<readonly Polymiddleware[]>(
6467
() =>
6568
Object.freeze([
66-
createChatLauncherButtonPolymiddleware(() => () => chatLauncherButtonComponent(ChatLauncherButton))
69+
createChatLauncherButtonPolymiddleware(() => () => chatLauncherButtonComponent(ChatLauncherButton)),
70+
createIconButtonPolymiddleware(() => () => iconButtonComponent(IconButton))
6771
]),
6872
[]
6973
);
Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,8 @@
11
:global(.webchat-experience-chat-launcher) .chat-launcher-button {
2-
/* TODO: [P0] Expose some CSS classes to white-label. */
3-
--webchat-colorNeutralBackground1: var(--colorNeutralBackground1, #ffffff);
4-
--webchat-colorNeutralForeground1: var(--colorNeutralForeground1, #242424);
5-
--webchat-colorNeutralStroke1: var(--colorNeutralStroke1, #d1d1d1);
6-
--webchat-shadow8: var(--shadow8, 0 0 2px rgba(0, 0, 0, 0.12), 0 4px 8px rgba(0, 0, 0, 0.14));
7-
8-
/* TODO: [P0] Should we expose some customizable CSS variables? */
9-
/* TODO: [P0] How's the naming system? */
10-
/* TODO: [P0] Should it be customizable to anchor to bottom/left instead? */
11-
--webchat-chatLauncherButton-background: var(--webchat-colorNeutralBackground1);
12-
--webchat-chatLauncherButton-borderColor: var(--webchat-colorNeutralStroke1);
13-
--webchat-chatLauncherButton-borderWidth: 1px;
14-
--webchat-chatLauncherButton-color: var(--webchat-colorNeutralForeground1);
152
--webchat-chatLauncherButton-margin: 40px;
16-
--webchat-chatLauncherButton-shadow: var(--webchat-shadow8);
173

18-
appearance: none;
19-
background: var(--webchat-chatLauncherButton-background);
20-
border: 0;
21-
border-radius: 30px;
224
bottom: 0;
23-
box-shadow: var(--webchat-chatLauncherButton-shadow);
24-
color: var(--webchat-chatLauncherButton-color);
25-
outline: var(--webchat-chatLauncherButton-borderWidth) solid var(--webchat-chatLauncherButton-borderColor);
265
position: fixed;
27-
height: 60px;
286
margin: var(--webchat-chatLauncherButton-margin);
29-
padding: 0;
307
right: 0;
31-
width: 60px;
328
}

0 commit comments

Comments
 (0)