@workday/canvas-kit-docs 8.2.1 → 8.2.2

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.
@@ -15,51 +15,20 @@ concepts. We will cover:
15
15
 
16
16
  ## Models
17
17
 
18
- A model is composed of state and events. State and Event shapes are as follows:
18
+ A model is composed of state and events. The shape of the model used by components looks like this:
19
19
 
20
20
  ```tsx
21
- type State = Record<string, any>;
22
- type Events = Record<string, (data?: object) => void>;
23
- ```
24
-
25
- The shape of a model is as follows:
26
-
27
- ```tsx
28
- interface Model<S extends State, E extends Events> {
29
- state: S;
30
- events: E;
31
- }
32
- ```
33
-
34
- The `@workday/canvas-kit-react/common` module exports a `Model` type for us.
35
-
36
- Let's start by defining our state and events:
37
-
38
- ```tsx
39
- // useDisclosureModel.tsx
40
- import React from 'react';
41
-
42
- import {Model} from '@workday/canvas-kit-react/common';
43
-
44
- export type DisclosureState = {
45
- visible: boolean;
46
- };
47
-
48
- export type DisclosureEvents = {
49
- show(data?: {}): void;
50
- hide(data?: {}): void;
21
+ type Model = {
22
+ state: Record<string, any>;
23
+ events: Record<string, (data?: any) => void>;
51
24
  };
52
-
53
- export type DisclosureModel = Model<DisclosureState, DisclosureEvents>;
54
25
  ```
55
26
 
56
- Let's add an `initialVisible` config and export a model hook:
27
+ Our model hook will take a config for `initialVisible` and return a model.
57
28
 
58
29
  ```tsx
59
- // useDisclosureModel.tsx
60
- // ...
61
-
62
- export type DisclosureConfig = {
30
+ // useDisclosureModel.ts
31
+ type DisclosureConfig = {
63
32
  initialVisible?: boolean;
64
33
  };
65
34
 
@@ -83,10 +52,10 @@ export const useDisclosureModel = (config: DisclosureConfig = {}) => {
83
52
  };
84
53
  ```
85
54
 
86
- Models aren't very complicated so far. We have a single `visible` state property and `show` and
87
- `hide` events we can send to the model. So far using the model might look like this:
55
+ The model has a single `visible` state property and `show` and `hide` events we can send to the
56
+ model. So far using the model might look like this:
88
57
 
89
- ```tsx
58
+ ```jsx
90
59
  const Test = () => {
91
60
  const model = useDisclosureModel();
92
61
 
@@ -114,56 +83,56 @@ You can find a working example here: https://codesandbox.io/s/basic-disclosure-m
114
83
  It would be nice to add guards and callbacks to our events. Let's add configuration to our model:
115
84
 
116
85
  ```tsx
117
- export type DisclosureConfig = {
86
+ type DisclosureConfig = {
118
87
  initialVisible?: boolean;
119
88
  // guards
120
- shouldShow?(event: {data?: {}; state: DisclosureState}): boolean;
121
- shouldHide?(event: {data?: {}; state: DisclosureState}): boolean;
89
+ shouldShow?(data: void, state: DisclosureState): boolean;
90
+ shouldHide?(data: void, state: DisclosureState): boolean;
122
91
  // callbacks
123
- onShow?(event: {data?: {}; prevState: DisclosureState}): void;
124
- onHide?(event: {data?: {}; prevState: DisclosureState}): void;
92
+ onShow?(data: void, prevState: DisclosureState): void;
93
+ onHide?(data: void, prevState: DisclosureState): void;
125
94
  };
126
95
  ```
127
96
 
128
97
  We'll also have to add the runtime of the guards and actions:
129
98
 
130
99
  ```tsx
131
- const events: DisclosureEvents = {
132
- show(data) {
133
- if (config.shouldShow?.({data, state}) === false) {
100
+ const events = {
101
+ show() {
102
+ if (config.shouldShow?.(undefined, state) === false) {
134
103
  return;
135
104
  }
136
105
  setVisible(true);
137
- config.onShow?.({data, prevState: state});
106
+ config.onShow?.(undefined, state);
138
107
  },
139
- hide(data) {
140
- if (config.shouldHide?.({data, state}) === false) {
108
+ hide() {
109
+ if (config.shouldHide?.(undefined, state) === false) {
141
110
  return;
142
111
  }
143
112
  setVisible(false);
144
- config.onHide?.({data, prevState: state});
113
+ config.onHide?.(undefined, state);
145
114
  },
146
115
  };
147
116
  ```
148
117
 
149
118
  Now we should be able to configure the model via the guards and do something in the callbacks:
150
119
 
151
- ```tsx
120
+ ```jsx
152
121
  const Test = () => {
153
122
  const [should, setShould] = React.useState(true);
154
123
  const model = useDisclosureModel({
155
- shouldShow({data, state}) {
124
+ shouldShow(data, state) {
156
125
  console.log('shouldShow', data, state, should);
157
126
  return should;
158
127
  },
159
- shouldHide({data, state}) {
128
+ shouldHide(data, state) {
160
129
  console.log('shouldHide', data, state, should);
161
130
  return should;
162
131
  },
163
- onShow({data, prevState}) {
132
+ onShow(data, prevState) {
164
133
  console.log('onShow', data, prevState);
165
134
  },
166
- onHide({data, prevState}) {
135
+ onHide(data, prevState) {
167
136
  console.log('onHide', data, prevState);
168
137
  },
169
138
  });
@@ -206,71 +175,45 @@ You can see it in action here: https://codesandbox.io/s/basic-configurable-discl
206
175
  That's a lot of extra boilerplate code for actions and callbacks. Our events don't have any data,
207
176
  but if they did, we'd have to keep the event + guard and callback data types in sync. We are also
208
177
  creating the `events` object every render. We could use React refs and `React.useMemo` to decrease
209
- extra object creation. Luckily the common module has `createEventMap` and `useEventMap` functions to
210
- help us reduce boilerplate and reduce possibility of making mistakes.
178
+ extra object creation. Luckily, the common module has the `createModelHook` factory function to help
179
+ us reduce boilerplate and reduce the possibility of making mistakes.
211
180
 
212
- First we need to create an event map - a map of guard and callback functions to the events they need
213
- to be paired with. We'll use `createEventMap` to do this:
181
+ `createModelHook` creates a model and infers the config, state, and events. The callbacks and guard
182
+ types will automatically be inferred.
214
183
 
215
184
  ```tsx
216
- import {createEventMap} from '@workday/canvas-kit-react/common';
185
+ // useDisclosureModel.ts
186
+ import {createModelHook} from '@workday/canvas-kit-react/common';
217
187
 
218
- const disclosureEventMap = createEventMap<DisclosureEvents>()({
219
- guards: {
220
- shouldShow: 'show',
221
- shouldHide: 'hide',
222
- },
223
- callbacks: {
224
- onShow: 'show',
225
- onHide: 'hide',
188
+ export const useDisclosureModel = createModelHook({
189
+ defaultConfig: {
190
+ initialVisible: false,
226
191
  },
227
- });
228
- ```
229
-
230
- This part is a little weird: `createEventMap<DisclosureEvents>()({`. The reason for this is a
231
- Typescript issue: https://github.com/microsoft/TypeScript/issues/26242. The gist is a function with
232
- generics requires the caller to specify none of the generics or all of them. In this case, the only
233
- generic that cannot be inferred is the `DisclosureEvents`. Everything else can be inferred. The only
234
- way to separate defined generics vs inferred generics is to have separate, chained functions.
235
-
236
- We see that `createEventMap` takes two optional keys: `guards` and `callbacks`. The names of these
237
- functions are arbitrary, but should follow the convention of `should*` for guards and `on*` for
238
- callbacks. The event name that is passed in is type checked against the `DisclosureEvents`
239
- interface.
240
-
241
- Now that we have an event map, we'll need to use it for our `DisclosureConfig`:
242
-
243
- ```tsx
244
- import {ToModelConfig} from '@workday/canvas-kit-react/common';
245
-
246
- export type DisclosureConfig = {
247
- initialVisible?: boolean;
248
- } & Partial<ToModelConfig<DisclosureState, DisclosureEvents, typeof disclosureEventMap>>;
249
- ```
192
+ })(config => {
193
+ const [visible, setVisible] = React.useState(config.initialVisible || false);
250
194
 
251
- The `ToModelConfig` type takes in our `State` type, `Events` type, and our `EventMap` type (the
252
- event map type is extracted from the event map: `typeof disclosureEventMap`). This will give us the
253
- proper shape of our config object.
195
+ const state = {
196
+ visible,
197
+ };
254
198
 
255
- The `disclosureEventMap` will also be used to create the `events` object using the `useEventMap`
256
- utility hook:
199
+ const events = {
200
+ show() {
201
+ setVisible(true);
202
+ },
203
+ hide() {
204
+ setVisible(false);
205
+ },
206
+ };
257
207
 
258
- ```tsx
259
- const events = useEventMap(disclosureEventMap, state, config, {
260
- show(data) {
261
- setVisible(true);
262
- },
263
- hide(data) {
264
- setVisible(false);
265
- },
208
+ return {state, events};
266
209
  });
267
210
  ```
268
211
 
269
- You can see `useEventMap` takes in our `disclosureEventMap`, `state`, `config` objects as well as a
270
- list of event implementations. This is all type checked, decreasing the chances we make a mistake.
271
- Also notice we don't need to implement guards and callbacks directly inside our event
272
- implementations. `useEventMap` will return an object that has that functionality built right in!
273
- Neat!
212
+ `createModelHook` takes a config object to determine the default config and the required config. We
213
+ only need default config. This function returns a function with a `config` object with all config
214
+ defaults applied. This is the body of the `useDisclosureModel` hook from earlier. Notice we don't
215
+ need to implement guards and callbacks directly inside our event implementations. `createModelHook`
216
+ will return an object that has that functionality built right in! Neat!
274
217
 
275
218
  The full working implementation is here:
276
219
  https://codesandbox.io/s/configurable-disclosure-model-3y5qh
@@ -303,14 +246,15 @@ import React from 'react';
303
246
 
304
247
  import {DisclosureTarget} from './DisclosureTarget';
305
248
  import {DisclosureContent} from './DisclosureContent';
249
+ import {useDisclosureModel} from './useDisclosureModel';
306
250
 
307
- import {DisclosureConfig, DisclosureModel, useDisclosureModel} from './useDisclosureModel';
251
+ type DisclosureConfig = typeof useDisclosureModel.TConfig;
308
252
 
309
253
  export interface DisclosureProps extends DisclosureConfig {
310
254
  children: React.ReactNode;
311
255
  }
312
256
 
313
- export const DisclosureModelContext = React.createContext({} as DisclosureModel);
257
+ const DisclosureModelContext = useDisclosureModel.Context;
314
258
 
315
259
  export const Disclosure = ({children, ...config}: DisclosureProps) => {
316
260
  const model = useDisclosureModel(config);
@@ -324,14 +268,14 @@ Disclosure.Target = DisclosureTarget;
324
268
  Disclosure.Content = DisclosureContent;
325
269
  ```
326
270
 
327
- We can see that the `DisclosureProps` interface extends the `DisclosureConfig` interface. This
328
- allows us to pass model config directly to the `<Disclosure>` component. A user of this
329
- `<Disclosure>` component might want to register a callback when the `show` event is called, for
330
- instance.
271
+ We can see that the `DisclosureProps` interface extends the config of `useDisclosureModel`.
272
+ `createModelHook` exposes a `TConfig` property to capture the config type. This allows us to pass
273
+ the model config directly to the `<Disclosure>` component. A user of this `<Disclosure>` component
274
+ might want to register a callback when the `show` event is called, for instance.
331
275
 
332
- Next, a React Context object is created to represent the model of the compound component. This
333
- context will be used to pass the model to sub-components implicitly. This allows our compound
334
- component API to remain clean for consumers of compound components.
276
+ The `createModelHook` creates a React Context that can be used by the `Disclosure` component to
277
+ expose the disclosure model to subcomponents without having to pass it via props. This allows our
278
+ compound component API to remain clean for consumers of compound components.
335
279
 
336
280
  In this particular compound component, the container component doesn't have a real element.
337
281
  Accessibility specifications have no `role` for this component, so an element is not required.
@@ -343,14 +287,16 @@ Let's go ahead and finish out our sub-components.
343
287
  ```tsx
344
288
  // DisclosureTarget.tsx
345
289
  import React from 'react';
346
- import {DisclosureModelContext} from './Disclosure';
290
+ import React from 'react';
291
+
292
+ import {useDisclosureModel} from './useDisclosureModel';
347
293
 
348
294
  export interface DisclosureTargetProps {
349
295
  children: React.ReactNode;
350
296
  }
351
297
 
352
298
  export const DisclosureTarget = ({children}: DisclosureTargetProps) => {
353
- const model = React.useContext(DisclosureModelContext);
299
+ const model = React.useContext(useDisclosureModel.Context);
354
300
 
355
301
  return (
356
302
  <button
@@ -376,14 +322,15 @@ event on the model.
376
322
  ```tsx
377
323
  // DisclosureContent.tsx
378
324
  import React from 'react';
379
- import {DisclosureModelContext} from './Disclosure';
325
+
326
+ import {useDisclosureModel} from './useDisclosureModel';
380
327
 
381
328
  export interface DisclosureContentProps {
382
329
  children: React.ReactNode;
383
330
  }
384
331
 
385
332
  export const DisclosureContent = ({children}: DisclosureContentProps) => {
386
- const model = React.useContext(DisclosureModelContext);
333
+ const model = React.useContext(useDisclosureModel.Context);
387
334
 
388
335
  return <div hidden={model.state.visible ? undefined : true}>{children}</div>;
389
336
  };
@@ -395,18 +342,23 @@ set a `hidden` attribute.
395
342
  The working example can be found here:
396
343
  https://codesandbox.io/s/configurable-disclosure-model-components-nvhtv
397
344
 
398
- These components are not fully compliant yet. They do not support `ref`, `as`, or extra props as
399
- HTML attributes. The boilerplate to supporting all this gets very complicated. For this reason, a
400
- `createComponent` utility function was created to support all this out of the box. `createComponent`
401
- takes a default `React.ElementType` which can be an element string like `div` or `button` or a
345
+ These components are not fully compliant yet. They do not support `model`, `ref`, `as`, or extra
346
+ props as HTML attributes. Also, we have to use `typeof` to create types and a `DisclosureContext`
347
+ variable (capitalized for JSX). We also have to worry about the `model` prop. The boilerplate for
348
+ supporting all of this gets very complicated. For this reason, `createContainer` and
349
+ `createSubcomponent` were created to handle this boilerplate for you out of the box. Both functions
350
+ take a default `React.ElementType` which can be an element string like `div` or `button` or a
402
351
  component like `Button`. It also takes a config object containing the following:
403
352
 
404
353
  - `displayName`: This will be the name of the component when shown by the React Dev tools. By
405
354
  convention, we make that name be the same as typed in a render function. For example
406
355
  `Disclosure.Target` vs `DisclosureTarget`.
407
- - `Component`: A [forward ref component function](https://reactjs.org/docs/forwarding-refs.html)
408
- with an added `Element` property. `Element` is the value passed to the Component's `as` prop. It
409
- will default to the provided element.
356
+ - `modelHook`: This is the model hook used by the compound component (`useDisclosureModel` in our
357
+ case). This model hook is used to determine proper prop types and seamlessly handle the option
358
+ `model` prop. For `createContainer`, if a `model` is not passed, a model is created and added to
359
+ React Context. For `createSubcomponent`, if a `model` is not passed, the model comes from React
360
+ Context.
361
+ - `elemPropsHook`: This is the elemPropsHook that takes a model and elemProps and returns elemProps.
410
362
  - `subComponents`: For container components. A list of sub components to add to the returned
411
363
  component. For example, a sub component called `DisclosureTarget` will be added to the export of
412
364
  `Disclosure` so that the user can import only `Disclosure` and use `Disclosure.Target`.
@@ -414,102 +366,94 @@ component like `Button`. It also takes a config object containing the following:
414
366
  interfaces. `Disclosure.Target = DisclosureTarget` will caused a type error. This property allows
415
367
  the `createComponent` factory function to infer the final interface of the returned component.
416
368
 
417
- Let's convert the Disclosure example to use the `createComponent` utility function to get this extra
369
+ Finally, a generic function is returned that takes the component configuration. The first argument
370
+ is `elemProps` with `ref` and hook props already merged in with props handed to the component. The
371
+ model config props will already be filtered out. We'll worry about `elemPropsHook` later. The second
372
+ is an `Element` property. `Element` is the value passed to the Component's `as` prop. It will
373
+ default to the provided element. The last parameter is an optional `model` reference. Ideally, the
374
+ model is used in `elemPropsHook` and therefore not normally needed inside the render function.
375
+
376
+ Let's convert the Disclosure example to use the `createContainer` utility function to get this extra
418
377
  functionality:
419
378
 
420
379
  ```tsx
421
380
  // Disclosure.tsx
422
381
  import React from 'react';
423
- import {createComponent} from '@workday/canvas-kit-react/common';
382
+ import {createContainer} from '@workday/canvas-kit-react/common';
383
+
424
384
  import {DisclosureTarget} from './DisclosureTarget';
425
385
  import {DisclosureContent} from './DisclosureContent';
386
+ import {useDisclosureModel} from './useDisclosureModel';
426
387
 
427
- import {DisclosureConfig, DisclosureModel, useDisclosureModel} from './useDisclosureModel';
388
+ export interface DisclosureProps {}
428
389
 
429
- export interface DisclosureProps extends DisclosureConfig {
430
- children: React.ReactNode;
431
- }
432
-
433
- export const DisclosureModelContext = React.createContext({} as DisclosureModel);
434
-
435
- export const Disclosure = createComponent()({
390
+ export const Disclosure = createContainer()({
436
391
  displayName: 'Disclosure',
437
- Component: ({children, ...config}: DisclosureProps) => {
438
- const model = useDisclosureModel(config);
439
-
440
- return (
441
- <DisclosureModelContext.Provider value={model}>{children}</DisclosureModelContext.Provider>
442
- );
443
- },
392
+ modelHook: useDisclosureModel,
444
393
  subComponents: {
445
394
  Target: DisclosureTarget,
446
395
  Content: DisclosureContent,
447
396
  },
397
+ })<DisclosureProps>(({children}) => {
398
+ return <>{children}</>;
448
399
  });
449
400
  ```
450
401
 
402
+ Notice we do not need to add `children` or `model` to our prop definition. `createContainer` is
403
+ adding those prop types for us. The `displayName` helps identify the component in React developer
404
+ tools. This is only needed by container components. The `subComponents` automatically adds a
405
+ `displayName` to subcomponents using the property key. For example, our `DisclosureTarget` will have
406
+ a `displayName` of `Disclosure.Target`. You can still provide a `displayName` to override this
407
+ naming convention.
408
+
451
409
  ```tsx
452
410
  // DisclosureTarget.tsx
453
411
  import React from 'react';
454
- import {createComponent} from '@workday/canvas-kit-react/common';
455
- import {DisclosureModelContext} from './Disclosure';
412
+ import {createSubcomponent} from '@workday/canvas-kit-react/common';
456
413
 
457
- export interface DisclosureTargetProps {
458
- children: React.ReactNode;
459
- }
414
+ import {useDisclosureModel} from './useDisclosureModel';
460
415
 
461
- export const DisclosureTarget = createComponent('button')({
462
- displayName: 'Disclosure.Target',
463
- Component: ({children, ...elemProps}: DisclosureTargetProps, ref, Element) => {
464
- const model = React.useContext(DisclosureModelContext);
416
+ export interface DisclosureTargetProps {}
465
417
 
466
- return (
467
- <Element
468
- ref={ref}
469
- onClick={() => {
470
- if (model.state.visible) {
471
- model.events.hide();
472
- } else {
473
- model.events.show();
474
- }
475
- }}
476
- {...elemProps}
477
- >
478
- {children}
479
- </Element>
480
- );
481
- },
418
+ export const DisclosureTarget = createSubcomponent('button')({
419
+ modelHook: useDisclosureModel,
420
+ })<DisclosureTargetProps>((elemProps, Element, model) => {
421
+ return (
422
+ <Element
423
+ onClick={() => {
424
+ if (model.state.visible) {
425
+ model.events.hide();
426
+ } else {
427
+ model.events.show();
428
+ }
429
+ }}
430
+ {...elemProps}
431
+ />
432
+ );
482
433
  });
483
434
  ```
484
435
 
485
436
  ```tsx
486
437
  // DisclosureContent.tsx
487
438
  import React from 'react';
488
- import {createComponent} from '@workday/canvas-kit-react/common';
489
- import {DisclosureModelContext} from './Disclosure';
439
+ import {createSubcomponent} from '@workday/canvas-kit-react/common';
490
440
 
491
- export interface DisclosureContentProps {
492
- children: React.ReactNode;
493
- }
441
+ import {useDisclosureModel} from './useDisclosureModel';
494
442
 
495
- export const DisclosureContent = createComponent('div')({
496
- displayName: 'Disclosure.Content',
497
- Component: ({children, ...elemProps}: DisclosureContentProps, ref, Element) => {
498
- const model = React.useContext(DisclosureModelContext);
443
+ export interface DisclosureContentProps {}
499
444
 
500
- return (
501
- <Element ref={ref} hidden={model.state.open ? undefined : true} {...elemProps}>
502
- {children}
503
- </Element>
504
- );
505
- },
445
+ export const DisclosureContent = createSubcomponent('div')({
446
+ modelHook: useDisclosureModel,
447
+ })<DisclosureContentProps>(({children, ...elemProps}, Element, model) => {
448
+ return (
449
+ <Element hidden={model.state.visible ? undefined : true} {...elemProps}>
450
+ {children}
451
+ </Element>
452
+ );
506
453
  });
507
454
  ```
508
455
 
509
- The `displayName` of the components helps properly identify the sub-components by their used name.
510
- For example `Disclose.Target` instead of `DiscloseTarget`.
511
-
512
- The `as` prop is being passed to the 3rd argument in the and we're calling it `Element`. The
456
+ The `as` prop is being passed to the second argument in the and we're calling it `Element`. The
513
457
  variable is passed to JSX as `<Element>`. `Element` is capitalized because the JSX parser treats
514
458
  capitalized elements as variables and lower case elements as strings:
515
459
 
@@ -530,8 +474,9 @@ render `as` as an element. If we were using Emotion's `styled` components, we'd
530
474
  not. Use `<Element>` when styling should come from the passed in element and use
531
475
  `<StyledElement as={Element}>` when the component handles styling.
532
476
 
533
- `createComponent` returns a component with a type interface that includes ref forwarding, the `as`
534
- prop for changing the underlying element, and additional props the element type normally takes.
477
+ `createContainer` and `createSubcomponent` return a component with a type interface that includes
478
+ ref forwarding, the `as` prop for changing the underlying element, the `model` prop, and additional
479
+ attributes/props the element type normally takes.
535
480
 
536
481
  For example, we can now do the following:
537
482
 
@@ -559,7 +504,7 @@ common so let's make a new model and compose from it instead. We'll later use th
559
504
  reusable behavioral hook.
560
505
 
561
506
  ```tsx
562
- // useIDModel.tsx
507
+ // useIDModel.ts
563
508
  import {Model, useUniqueId} from '@workday/canvas-kit-react/common';
564
509
 
565
510
  export type IDState = {
@@ -593,21 +538,19 @@ Also later we'll add behavioral hook that will require this model.
593
538
  Let's update the `DisclosureModel` to compose the `IDModel`:
594
539
 
595
540
  ```tsx
596
- // useDisclosureModel.tsx
597
- // ...
598
- import {IDState, IDConfig, useIDModel} from './useIDModel';
541
+ // useDisclosureModel.ts
542
+ import React from 'react';
599
543
 
600
- export type DisclosureState = IDState & {
601
- visible: boolean;
602
- };
544
+ import {createModelHook} from '@workday/canvas-kit-react/common';
603
545
 
604
- // ...
546
+ import {useIDModel} from './useIDModel';
605
547
 
606
- export type DisclosureConfig = IDConfig & {
607
- initialVisible?: boolean;
608
- } & Partial<ToModelConfig<DisclosureState, DisclosureEvents, typeof disclosureEventMap>>;
609
-
610
- export const useDisclosureModel = (config: DisclosureConfig = {}) => {
548
+ export const useDisclosureModel = createModelHook({
549
+ defaultConfig: {
550
+ ...useIDModel.defaultConfig,
551
+ initialVisible: false,
552
+ },
553
+ })(config => {
611
554
  const [visible, setVisible] = React.useState(config.initialVisible || false);
612
555
  const idModel = useIDModel(config);
613
556
 
@@ -616,10 +559,18 @@ export const useDisclosureModel = (config: DisclosureConfig = {}) => {
616
559
  visible,
617
560
  };
618
561
 
619
- // ...
562
+ const events = {
563
+ ...idModel.events,
564
+ show() {
565
+ setVisible(true);
566
+ },
567
+ hide() {
568
+ setVisible(false);
569
+ },
570
+ };
620
571
 
621
572
  return {state, events};
622
- };
573
+ });
623
574
  ```
624
575
 
625
576
  We can now add `aria-controls` to `DisclosureTarget` and `id` to `DisclosureContent`. We'll also add
@@ -632,7 +583,6 @@ We can now add `aria-controls` to `DisclosureTarget` and `id` to `DisclosureCont
632
583
 
633
584
  return (
634
585
  <Element
635
- ref={ref}
636
586
  aria-controls={model.state.id}
637
587
  aria-expanded={model.state.visible}
638
588
  onClick={() => {
@@ -657,12 +607,7 @@ return (
657
607
  // ...
658
608
 
659
609
  return (
660
- <Element
661
- ref={ref}
662
- id={model.state.id}
663
- hidden={model.state.visible ? undefined : true}
664
- {...elemProps}
665
- >
610
+ <Element id={model.state.id} hidden={model.state.visible ? undefined : true} {...elemProps}>
666
611
  {children}
667
612
  </Element>
668
613
  );
@@ -685,10 +630,14 @@ UI of a dropdown menu look very different!
685
630
  We'll build a behavior hook for the `DisclosureTarget` component:
686
631
 
687
632
  ```tsx
688
- // useExpandableControls.tsx
689
- import {DisclosureModel} from './useDisclosureModel';
690
-
691
- export const useExpandableControls = ({state}: DisclosureModel, elemProps: {}) => {
633
+ // useExpandableControls.ts
634
+ import {useDisclosureModel} from './useDisclosureModel';
635
+
636
+ export const useExpandableControls = (
637
+ {state}: ReturnType<typeof useDisclosureModel>,
638
+ elemProps = {},
639
+ ref?: React.Ref<any>
640
+ ) => {
692
641
  return {
693
642
  'aria-controls': state.id,
694
643
  'aria-expanded': state.visible,
@@ -706,11 +655,15 @@ won't get into that here, but it is useful and works with `composeHooks` that is
706
655
  `common` module. Let's refactor the above to use that function:
707
656
 
708
657
  ```tsx
709
- // useExpandableControls.tsx
658
+ // useExpandableControls.ts
710
659
  import {mergeProps} from '@workday/canvas-kit-react/common';
711
- import {DisclosureModel} from './useDisclosureModel';
660
+ import {useDisclosureModel} from './useDisclosureModel';
712
661
 
713
- export const useExpandableControls = ({state}: DisclosureModel, elemProps: {}) => {
662
+ export const useExpandableControls = (
663
+ {state}: ReturnType<typeof useDisclosureModel>,
664
+ elemProps = {},
665
+ ref?: React.Ref<any>
666
+ ) => {
714
667
  return mergeProps(
715
668
  {
716
669
  'aria-controls': state.id,
@@ -724,73 +677,167 @@ export const useExpandableControls = ({state}: DisclosureModel, elemProps: {}) =
724
677
  Even though the `useExpandableControls` did not use any special props that need special merging, it
725
678
  is a good habit to use `mergeProps` anytime you define props.
726
679
 
727
- Now we can use the behavior hook in the `DiscloseTarget` component:
680
+ This is still a lot of boilerplate. We need the return type of the model hook, we need to specify
681
+ that our hook can optionally accept `elemProps` and a `ref`, and we need to call `mergeProps`.
682
+ `createElemPropsHook` helps with a lot of this boilerplate:
728
683
 
729
684
  ```tsx
730
- // DisclosureTarget.tsx
731
- import {createComponent, mergeProps} from '@workday/canvas-kit-react/common';
685
+ import {createElemPropsHook} from '@workday/canvas-kit-react/common';
686
+ import {useDisclosureModel} from './useDisclosureModel';
687
+
688
+ export const useExpandableControls = createElemPropsHook(useDisclosureModel)(({state}) => {
689
+ return {
690
+ 'aria-controls': state.id,
691
+ 'aria-expanded': state.visible,
692
+ };
693
+ });
694
+ ```
695
+
696
+ `createElemPropsHook` takes the model hook and an elem props hook body as arguments. The hook
697
+ function body doesn't need to call `mergeProps` since `createElemPropsHook` takes care of that for
698
+ us. Our logic can focus only on the props we need to add to an element!
699
+
700
+ Now we have a reusable elemProps hook that can be composed into other hooks or used on its own.
701
+ "expandable controls" could be used on a select component, a popup component, or any other type of
702
+ disclosure target component. We don't add the `onClick` because how the disclosure is revealed
703
+ depends on the disclosure target type. In a `Select` component, that could be by clicking on the
704
+ target, or using the down arrow. On a `Tooltip` component, it could be revealed by a mouse hover or
705
+ focus event. Lets create a `useDisclosureTarget` elemProps hook that merges in an `onClick` with
706
+ `useExpandableControls`:
707
+
708
+ ```tsx
709
+ // useDisclosureTarget.ts
710
+ import {createElemPropsHook, mergeProps} from '@workday/canvas-kit-react/common';
711
+ import {useDisclosureModel} from './useDisclosureModel';
732
712
  import {useExpandableControls} from './useExpandableControls';
733
713
 
734
- // ...
735
- const model = React.useContext(DisclosureModelContext);
736
- const props = mergeProps(
737
- {
738
- onClick() {
739
- if (model.state.visible) {
740
- model.events.hide();
741
- } else {
742
- model.events.show();
743
- }
744
- },
745
- },
746
- useExpandableControls(model, elemProps)
714
+ export const useDisclosureTarget = createElemPropsHook(useDisclosureModel)(
715
+ (model, ref, elemProps) => {
716
+ const props = useExpandableControls(model, elemProps, ref);
717
+
718
+ return mergeProps(
719
+ {
720
+ onClick() {
721
+ if (model.state.visible) {
722
+ model.events.hide();
723
+ } else {
724
+ model.events.show();
725
+ }
726
+ },
727
+ },
728
+ props
729
+ );
730
+ }
747
731
  );
732
+ ```
748
733
 
749
- return (
750
- <Element ref={ref} {...props}>
751
- {children}
752
- </Element>
734
+ Notice we still need to use `mergeProps` to compose the behavior of our two elemProps hooks?
735
+ `composeHooks` was created to handle this common composition use case. `composeHooks` takes two or
736
+ more elemProps hooks and returns a new hook with all props merged for us:
737
+
738
+ ```tsx
739
+ // useDisclosureTarget.ts
740
+ import {createElemPropsHook, composeHooks} from '@workday/canvas-kit-react/common';
741
+ import {useDisclosureModel} from './useDisclosureModel';
742
+ import {useExpandableControls} from './useExpandableControls';
743
+
744
+ export const useDisclosureTarget = composeHooks(
745
+ createElemPropsHook(useDisclosureModel)(model => {
746
+ return {
747
+ onClick() {
748
+ if (model.state.visible) {
749
+ model.events.hide();
750
+ } else {
751
+ model.events.show();
752
+ }
753
+ },
754
+ };
755
+ }),
756
+ useExpandableControls
753
757
  );
754
- // ...
755
758
  ```
756
759
 
757
- We used `mergeProps` for the `onClick`. This way, if the user passes their own `onClick`, their
758
- `onClick` will be called in addition to the `onClick` we've defined here. Without this, if the user
759
- passes an `onClick`, it will override ours and break functionality. Definitely not what we want!
760
+ We don't even need to declare `elemProps` or `ref` parameters if we don't use them!
760
761
 
761
- We'll also make a `useHidden` behavior hook for the `hidden` attribute on the `Disclosure.Content`
762
- element:
762
+ Now we can use the behavior hook in the `DiscloseTarget` component:
763
763
 
764
764
  ```tsx
765
- // useHidden.tsx
766
- import {mergeProps} from '@workday/canvas-kit-react/common';
767
- import {DisclosureModel} from './useDisclosureModel';
765
+ // DisclosureTarget.tsx
766
+ import React from 'react';
767
+ import {createSubcomponent} from '@workday/canvas-kit-react/common';
768
768
 
769
- export const useHidden = ({state}: DisclosureModel, elemProps: {}) => {
770
- return mergeProps(
771
- {
772
- hidden: state.visible ? undefined : true,
773
- },
774
- elemProps
775
- );
776
- };
769
+ import {useDisclosureModel} from './useDisclosureModel';
770
+ import {useDisclosureTarget} from './useDisclosureTarget';
771
+
772
+ export interface DisclosureTargetProps {}
773
+
774
+ export const DisclosureTarget = createSubcomponent('button')({
775
+ modelHook: useDisclosureModel,
776
+ })<DisclosureTargetProps>((elemProps, Element, model) => {
777
+ const props = useDisclosureTarget(model, elemProps);
778
+ return <Element {...props} />;
779
+ });
780
+ ```
781
+
782
+ Note: We should never use `createElemPropsHook` or `composeHooks` inside a render function as that
783
+ would be slower. Always hoist the hook definition outside a render function.
784
+
785
+ It is very common to use an elemProps hook with a compound component, so `createContainer` and
786
+ `createSubcomponent` both take an `elemPropsHook` configuration option. This way we don't have to
787
+ worry about the `model` or using `mergeProps` in our component definition. Here's the final code.
788
+
789
+ ```tsx
790
+ // DisclosureTarget.tsx
791
+ import React from 'react';
792
+ import {createSubcomponent} from '@workday/canvas-kit-react/common';
793
+
794
+ import {useDisclosureModel} from './useDisclosureModel';
795
+ import {useDisclosureTarget} from './useDisclosureTarget';
796
+
797
+ export interface DisclosureTargetProps {}
798
+
799
+ export const DisclosureTarget = createSubcomponent('button')({
800
+ modelHook: useDisclosureModel,
801
+ elemPropsHook: useDisclosureTarget,
802
+ })<DisclosureTargetProps>((elemProps, Element) => {
803
+ return <Element {...elemProps} />;
804
+ });
805
+ ```
806
+
807
+ We'll also make a `useDisclosureContent` behavior hook for the `hidden` attribute on the
808
+ `Disclosure.Content` element:
809
+
810
+ ```tsx
811
+ // useDisclosureContent.ts
812
+ import {createElemPropsHook} from '@workday/canvas-kit-react/common';
813
+ import {useDisclosureModel} from './useDisclosureModel';
814
+
815
+ export const useDisclosureContent = createElemPropsHook(useDisclosureModel)(model => {
816
+ return {
817
+ id: model.state.id,
818
+ hidden: model.state.visible ? undefined : true,
819
+ };
820
+ });
777
821
  ```
778
822
 
779
823
  The `Disclosure.Content` subcomponent can now be updated to use this hook:
780
824
 
781
825
  ```tsx
782
826
  // DisclosureContent.tsx
783
- import {useHidden} from './useHidden';
784
- // ..
827
+ import React from 'react';
828
+ import {createSubcomponent} from '@workday/canvas-kit-react/common';
785
829
 
786
- const model = React.useContext(DisclosureModelContext);
787
- const props = useHidden(model, elemProps);
830
+ import {useDisclosureModel} from './useDisclosureModel';
831
+ import {useDisclosureContent} from './useDisclosureContent';
788
832
 
789
- return (
790
- <Element ref={ref} id={model.state.id} {...props}>
791
- {children}
792
- </Element>
793
- );
833
+ export interface DisclosureContentProps {}
834
+
835
+ export const DisclosureContent = createSubcomponent('div')({
836
+ modelHook: useDisclosureModel,
837
+ elemPropsHook: useDisclosureContent,
838
+ })<DisclosureContentProps>(({children, ...elemProps}, Element) => {
839
+ return <Element {...elemProps}>{children}</Element>;
840
+ });
794
841
  ```
795
842
 
796
843
  The full code can be found here:
@@ -806,32 +853,22 @@ mouse and focus events.
806
853
  Here's a tooltip model composing the disclosure model:
807
854
 
808
855
  ```tsx
809
- // useTooltipModel.tsx
810
- import {Model} from '@workday/canvas-kit-react/common';
811
-
812
- import {
813
- DisclosureConfig,
814
- DisclosureEvents,
815
- DisclosureState,
816
- useDisclosureModel,
817
- } from './useDisclosureModel';
818
-
819
- export type TooltipState = DisclosureState;
820
- export type TooltipEvents = DisclosureEvents;
821
- export type TooltipModel = Model<TooltipState, TooltipEvents>;
822
-
823
- export type TooltipConfig = DisclosureConfig & {
824
- initialVisible?: never; // tooltips never start showing
825
- };
856
+ // useTooltipModel.ts
857
+ import {createModelHook} from '@workday/canvas-kit-react/common';
826
858
 
827
- export const useTooltipModel = (config: TooltipConfig = {}) => {
828
- const disclosure = useDisclosureModel(config);
859
+ import {useDisclosureModel} from './useDisclosureModel';
829
860
 
830
- const state = disclosure.state;
831
- const events = disclosure.events;
861
+ const {
862
+ initialVisible, // tooltips are never initially visible, so remove the option
863
+ ...defaultConfig
864
+ } = useDisclosureModel.defaultConfig;
832
865
 
833
- return {state, events};
834
- };
866
+ export const useTooltipModel = createModelHook({
867
+ defaultConfig,
868
+ requiredConfig: useDisclosureModel.requiredConfig,
869
+ })(config => {
870
+ return useDisclosureModel(config);
871
+ });
835
872
  ```
836
873
 
837
874
  Not much interesting is happening here. We're not adding additional state or events, but we're
@@ -851,29 +888,25 @@ The `Tooltip` container component looks almost exactly like the Disclosure compo
851
888
  ```tsx
852
889
  // Tooltip.tsx
853
890
  import React from 'react';
854
- import {createComponent} from '@workday/canvas-kit-react/common';
891
+ import {createContainer} from '@workday/canvas-kit-react/common';
855
892
 
856
- import {useTooltipModel, TooltipModel, TooltipConfig} from './useTooltipModel';
893
+ import {useTooltipModel} from './useTooltipModel';
857
894
  import {TooltipTarget} from './TooltipTarget';
858
895
  import {TooltipContent} from './TooltipContent';
859
896
 
860
- export const TooltipModelContext = React.createContext<TooltipModel>({} as any);
861
-
862
- export interface TooltipProps extends TooltipConfig {
863
- children: React.ReactNode;
897
+ export interface TooltipProps {
898
+ children?: React.ReactNode;
864
899
  }
865
900
 
866
- export const Tooltip = createComponent()({
901
+ export const Tooltip = createContainer()({
867
902
  displayName: 'Tooltip',
868
- Component: ({children, ...config}: TooltipProps) => {
869
- const model = useTooltipModel(config);
870
-
871
- return <TooltipModelContext.Provider value={model}>{children}</TooltipModelContext.Provider>;
872
- },
903
+ modelHook: useTooltipModel,
873
904
  subComponents: {
874
905
  Target: TooltipTarget,
875
906
  Content: TooltipContent,
876
907
  },
908
+ })(({children}: TooltipProps) => {
909
+ return <>{children}</>;
877
910
  });
878
911
  ```
879
912
 
@@ -881,49 +914,40 @@ The `Tooltip.Target` component is similar to the `DisclosureTarget` component, b
881
914
  behavior. The tooltip triggers on different events. Here's the code:
882
915
 
883
916
  ```tsx
917
+ // TooltipTarget.tsx
884
918
  import React from 'react';
885
- import {createComponent, mergeProps} from '@workday/canvas-kit-react/common';
919
+ import {createSubcomponent, createElemPropsHook} from '@workday/canvas-kit-react/common';
886
920
 
887
- import {TooltipModelContext} from './Tooltip';
888
- import {TooltipModel} from './useTooltipModel';
921
+ import {useTooltipModel} from './useTooltipModel';
889
922
 
890
923
  export interface TooltipTargetProps {
891
924
  children: React.ReactNode;
892
925
  }
893
926
 
894
- export const useTooltipTarget = ({state, events}: TooltipModel, elemProps = {}) => {
895
- return mergeProps(
896
- {
897
- onFocus(event: any) {
898
- events.show();
899
- },
900
- onBlur() {
901
- events.hide();
902
- },
903
- onMouseEnter() {
904
- events.show();
905
- },
906
- onMouseLeave() {
907
- events.hide();
908
- },
909
- 'aria-describedby': state.id,
927
+ export const useTooltipTarget = createElemPropsHook(useTooltipModel)(({state, events}) => {
928
+ return {
929
+ onFocus(event: any) {
930
+ events.show();
910
931
  },
911
- elemProps
912
- );
913
- };
932
+ onBlur() {
933
+ events.hide();
934
+ },
935
+ onMouseEnter() {
936
+ events.show();
937
+ },
938
+ onMouseLeave() {
939
+ events.hide();
940
+ },
941
+ 'aria-describedby': state.id,
942
+ };
943
+ });
914
944
 
915
- export const TooltipTarget = createComponent('button')({
945
+ export const TooltipTarget = createSubcomponent('button')({
916
946
  displayName: 'Tooltip.Target',
917
- Component: ({children, ...elemProps}: TooltipTargetProps, ref, Element) => {
918
- const model = React.useContext(TooltipModelContext);
919
- const props = useTooltipTarget(model, elemProps);
920
-
921
- return (
922
- <Element ref={ref} {...props}>
923
- {children}
924
- </Element>
925
- );
926
- },
947
+ modelHook: useTooltipModel,
948
+ elemPropsHook: useTooltipTarget,
949
+ })<TooltipTargetProps>(({children, ...elemProps}, Element) => {
950
+ return <Element {...elemProps}>{children}</Element>;
927
951
  });
928
952
  ```
929
953
 
@@ -933,41 +957,39 @@ comes from the `IDModel`.
933
957
  The `Tooltip.Content` component is similar to the `Disclosure.Content` component, except that it
934
958
  uses a ReactDOM portal to ensure the content appears on top of other content. This example doesn't
935
959
  include a positional library and instead hard-codes positional values. Notice we can reuse our
936
- `useHidden` behavior hook in this component!
960
+ `useDisclosureContent` behavior hook in this component!
937
961
 
938
962
  ```tsx
939
963
  import React from 'react';
940
964
  import ReactDOM from 'react-dom';
941
- import {createComponent, mergeProps} from '@workday/canvas-kit-react/common';
942
-
943
- import {TooltipModelContext} from './Tooltip';
944
- import {useHidden} from './useHidden';
945
-
946
- export interface TooltipContentProps {
947
- children: React.ReactNode;
948
- }
949
-
950
- export const TooltipContent = createComponent('div')({
951
- displayName: 'Tooltip.Content',
952
- Component: ({children, ...elemProps}: TooltipContentProps, ref, Element) => {
953
- const model = React.useContext(TooltipModelContext);
954
- const props = mergeProps(
955
- {
956
- id: model.state.id,
957
- style: {position: 'absolute', left: 80, top: 10},
958
- },
959
- useHidden(model, elemProps)
960
- );
965
+ import {
966
+ createSubcomponent,
967
+ createElemPropsHook,
968
+ composeHooks,
969
+ } from '@workday/canvas-kit-react/common';
970
+
971
+ import {useDisclosureContent} from './useDisclosureContent';
972
+ import {useTooltipModel} from './useTooltipModel';
973
+
974
+ export interface TooltipContentProps {}
975
+
976
+ const useTooltipContent = composeHooks(
977
+ createElemPropsHook(useTooltipModel)(model => {
978
+ return {
979
+ style: {position: 'absolute', left: 80, top: 10},
980
+ };
981
+ }),
982
+ useDisclosureContent
983
+ );
961
984
 
962
- return ReactDOM.createPortal(
963
- model.state.id ? (
964
- <Element ref={ref} {...props}>
965
- {children}
966
- </Element>
967
- ) : null,
968
- document.body
969
- );
970
- },
985
+ export const TooltipContent = createSubcomponent('div')({
986
+ modelHook: useTooltipModel,
987
+ elemPropsHook: useTooltipContent,
988
+ })<TooltipContentProps>(({children, ...elemProps}, Element, model) => {
989
+ return ReactDOM.createPortal(
990
+ model.state.id ? <Element {...elemProps}>{children}</Element> : null,
991
+ document.body
992
+ );
971
993
  });
972
994
  ```
973
995
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workday/canvas-kit-docs",
3
- "version": "8.2.1",
3
+ "version": "8.2.2",
4
4
  "description": "Documentation components of Canvas Kit components",
5
5
  "author": "Workday, Inc. (https://www.workday.com)",
6
6
  "license": "Apache-2.0",
@@ -42,7 +42,7 @@
42
42
  ],
43
43
  "dependencies": {
44
44
  "@storybook/csf": "0.0.1",
45
- "@workday/canvas-kit-react": "^8.2.1"
45
+ "@workday/canvas-kit-react": "^8.2.2"
46
46
  },
47
47
  "devDependencies": {
48
48
  "fs-extra": "^10.0.0",
@@ -50,5 +50,5 @@
50
50
  "mkdirp": "^1.0.3",
51
51
  "typescript": "4.1"
52
52
  },
53
- "gitHead": "2d5d8d9da4172399ae6e82166249af3d9589d10f"
53
+ "gitHead": "aa6a44ca27cc634530942790fe2c18dff0962778"
54
54
  }