Skip to content

Commit 8168b5b

Browse files
element/ak-empty-state: consistency check
# What - Removes the unused Interface declaration - Harmonizes the sizes: Component, Template, and Builder all agree “large” is the default - Change ‘no-icon’ boolean attribute to ‘text-only’ (HTML tries to “use positive names for attributes”) - Fixes some of the rendering issues in the builder: - Fix “secondary-actions” slot name - Use `<h2>` with string-only title - Use `<p>` with string-only body - Use `<ak-icon>` with string-only icon - Remove unloved console.log() :-) - Add story to Storybook “builder” section to show string-only usage
1 parent 374a40d commit 8168b5b

File tree

5 files changed

+72
-30
lines changed

5 files changed

+72
-30
lines changed

src/ak-empty-state/ak-empty-state.builder.ts

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@ import "./ak-empty-state.component.js";
22

33
import { EmptyState } from "./ak-empty-state.component.js";
44

5+
import { match, P } from "ts-pattern";
6+
57
import { html, TemplateResult } from "lit";
68
import { ifDefined } from "lit/directives/if-defined.js";
79

810
/* The `pick`ed fields here correspond to their types in the EmptyState class above. */
911

10-
export interface EmptyStateProps extends Partial<
11-
Pick<EmptyState, "size" | "loading" | "textOnly" | "spinnerOnly">
12-
> {
13-
fullHeight?: boolean;
12+
export interface EmptyStateSlots {
1413
icon?: string | TemplateResult;
1514
title?: string | TemplateResult;
1615
body?: string | TemplateResult;
@@ -19,39 +18,55 @@ export interface EmptyStateProps extends Partial<
1918
secondaryActions?: string | TemplateResult;
2019
}
2120

22-
const SLOTNAMES: (keyof EmptyStateProps)[] = [
21+
export type EmptyStateProps = Partial<
22+
Pick<EmptyState, "size" | "loading" | "textOnly" | "spinnerOnly">
23+
> &
24+
EmptyStateSlots & { fullHeight?: boolean };
25+
26+
const SLOTNAMES: (keyof EmptyStateSlots)[] = [
2327
"icon",
2428
"title",
2529
"body",
2630
"footer",
2731
"actions",
2832
"secondaryActions",
29-
];
33+
] as const;
34+
35+
type SlotName = (typeof SLOTNAMES)[number];
36+
37+
type SlotContent = string | TemplateResult | undefined;
3038

3139
/**
3240
* @summary Helper function to create an EmptyState component programmatically
3341
*
3442
* @returns {TemplateResult} A Lit template result containing the configured ak-empty-state element
3543
*
44+
* NOTE: This function does not edit TemplateResults passed in. If you pass in a TemplateResult, it
45+
* *must* indicated what slot it's being added to, even if you think the prop should handle it.
46+
*
3647
* @see {@link EmptyState} - The underlying web component
3748
*/
3849
export function akEmptyState(options: EmptyStateProps) {
3950
const slots = SLOTNAMES.filter((s) => !!options[s]);
4051

52+
const slotRenderer = (s: string, c: string | TemplateResult) => html`<div slot=${s}>${c}</div>`;
53+
54+
const slotHandler = (s: SlotName) =>
55+
match<[SlotContent, SlotName], SlotContent>([options[s], s])
56+
.with([P.string, "secondaryActions"], ([c]) => slotRenderer("secondary-actions", c))
57+
.with([P.string, "title"], ([c]) => html`<h2 slot="title">${c}</h2>`)
58+
.with([P.string, "body"], ([c]) => html`<p slot="body">${c}</p>`)
59+
.with(
60+
[P.string, "icon"],
61+
([option]) => html`<ak-icon slot="icon" icon=${option}></ak-icon>`,
62+
)
63+
.with([P.string, P._], ([option, s]) => slotRenderer(s, option))
64+
.otherwise(() => options[s]);
65+
4166
let opts = {
4267
...options,
43-
...Object.fromEntries(
44-
slots.map((s) => [
45-
s,
46-
typeof options[s] === "string"
47-
? html`<div slot=${s === "secondaryActions" ? "secondary-actions" : s}>
48-
${options[s]}
49-
</div>`
50-
: options[s],
51-
]),
52-
),
68+
...Object.fromEntries(slots.map((s) => [s, slotHandler(s)])),
5369
};
54-
console.log(opts);
5570

5671
const { size, fullHeight, spinnerOnly, textOnly, loading } = opts;
5772

src/ak-empty-state/ak-empty-state.component.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { property } from "lit/decorators.js";
1919
* @attr {boolean} spinner-only - Shows only the spinner, not the (localized) text "Loading..." when `loading` is true
2020
* @attr {boolean} text-only - Hides the default icon when true
2121
* @attr {boolean} full-height - Makes component take full height of container
22+
* @attr {string} icon - The name of an icon, as understood by ak-icon. NOTE: if both this attribute and the `icon` slot are used, the slot takes priority.
2223
*
2324
* @slot icon - Icon displayed at the top of the empty state
2425
* @slot title - Title describing the empty state

src/ak-empty-state/ak-empty-state.stories.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -262,8 +262,8 @@ export const SizeVariants: Story = {
262262
</div>
263263
264264
<div>
265-
<h3>Default (large)</h3>
266-
<ak-empty-state>
265+
<h3>Medium</h3>
266+
<ak-empty-state size="md">
267267
<h2 slot="title">No results found</h2>
268268
<p slot="body">
269269
No results match the filter criteria. Clear all filters and try again.
@@ -275,8 +275,8 @@ export const SizeVariants: Story = {
275275
</div>
276276
277277
<div>
278-
<h3>Large</h3>
279-
<ak-empty-state size="lg">
278+
<h3>Large (Default)</h3>
279+
<ak-empty-state>
280280
<h2 slot="title">No results found</h2>
281281
<p slot="body">
282282
No results match the filter criteria. Clear all filters and try again.
@@ -332,6 +332,30 @@ export const HelperFunction: Story = {
332332
><button>Secondary Action</button>`,
333333
footer: html`<a href="#" slot="footer">Learn more about this state</a>`,
334334
})}
335+
${akEmptyState({
336+
size: "lg",
337+
fullHeight: false,
338+
title: html`<h2 slot="title">AKIcon Token Usage</h2>`,
339+
icon: "fas fa-face-dizzy",
340+
body: html`<p slot="body">
341+
What the empty state looks like if you pass an ak-icon token to ak-empty-state's
342+
builder's
343+
<kbd>icon</kbd> field.
344+
</p>`,
345+
actions: html`<button slot="actions">Primary Action</button
346+
><button>Secondary Action</button>`,
347+
footer: html`<a href="#" slot="footer">Learn more about this state</a>`,
348+
})}
349+
${akEmptyState({
350+
size: "lg",
351+
fullHeight: false,
352+
title: "Just With Strings",
353+
icon: "fas fa-meteor",
354+
body: "Using the sweet meteor of doom to demonstrate what this looks like using mostly string arguments to the builder.",
355+
actions: html`<button slot="actions">Primary Action</button
356+
><button>Secondary Action</button>`,
357+
footer: "Nothing more need be said.",
358+
})}
335359
</div>
336360
`,
337361
};

src/ak-empty-state/ak-empty-state.template.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@ import { html, nothing } from "lit";
55

66
export const emptyStateSizes = ["xs", "sm", "md", "lg", "xl"] as const;
77
export type EmptyStateSize = (typeof emptyStateSizes)[number];
8-
const DEFAULT_SIZE_INDEX = emptyStateSizes.indexOf("md");
8+
const DEFAULT_SIZE_INDEX = emptyStateSizes.indexOf("lg");
99
const isEmptyStateSize = (s?: string): s is EmptyStateSize =>
1010
typeof s === "string" && s.trim() !== "" && emptyStateSizes.includes(s as EmptyStateSize);
1111

12+
// The size of the spinner and the icon do not automatically scale with the size of the EmptyState
13+
// object itself. After much experimentation, the design team has settled on the pattern below as
14+
// having the best aesthetics.
15+
1216
const spinnerSizes = ["md", "lg", "lg", "xl", "xl"];
1317
const iconSizes = ["sm", "md", "lg", "xl", "6x"];
1418

@@ -17,7 +21,7 @@ function iconTemplate(
1721
icon: string | undefined,
1822
skipIcon: boolean,
1923
isLoading: boolean,
20-
size: EmptyStateSize,
24+
size: EmptyStateSize
2125
) {
2226
if (useSlot) {
2327
return html`<div part="icon"><slot name="icon"></slot></div>`;
@@ -63,8 +67,7 @@ const secondaryActionsTemplate = (has: boolean) =>
6367
</div>`
6468
: nothing;
6569

66-
const footerTemplate = (has: boolean) =>
67-
has ? html`<div part="footer"><slot name="footer"></slot></div>` : nothing;
70+
const footerTemplate = (has: boolean) => (has ? html`<div part="footer"><slot name="footer"></slot></div>` : nothing);
6871

6972
interface EmptyStateTemplateProps {
7073
hasTitle: boolean;
@@ -109,8 +112,7 @@ export function template(props: EmptyStateTemplateProps) {
109112
${bodyTemplate(hasBody, showLoading)}
110113
${hasFooter
111114
? html` <div part="footer">
112-
${actionsTemplate(hasActions)}
113-
${secondaryActionsTemplate(hasSecondaryActions)}
115+
${actionsTemplate(hasActions)} ${secondaryActionsTemplate(hasSecondaryActions)}
114116
${footerTemplate(hasFooterContent)}
115117
</div>`
116118
: nothing}

src/ak-empty-state/ak-empty-state.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,15 +113,15 @@ describe("ak-empty-state component", () => {
113113
await expect.element(iconSlot).toBeInTheDocument();
114114
});
115115

116-
it("should render custom icon even when no-icon is true", async () => {
116+
it("should render custom icon even when text-only is true", async () => {
117117
render(
118-
html`<ak-empty-state no-icon>
118+
html`<ak-empty-state text-only>
119119
<div slot="icon">
120120
<svg viewBox="0 0 24 24">
121121
<path d="M12 2L1 21h22L12 2z" />
122122
</svg>
123123
</div>
124-
<h2 slot="title">Custom icon with no-icon</h2>
124+
<h2 slot="title">Custom icon with text-only setting</h2>
125125
</ak-empty-state>`,
126126
);
127127

0 commit comments

Comments
 (0)