Open aubrey-oneal opened 5 months ago
@ljcarot
I’ve updated the current task list to include the experimental components that we identified as utilities. Cascade
is currently considered stable so will wait until after #6165 to determine if there’s any additional requirements for stable to utility.
@aubrey-oneal As a follow up to some of our experimental component audit, we also felt like we should add a utility section, but I do think we should have more discussion on if/how we document utilities. #6157
here are some notes i've compiled while looking into this.
the main thing is that because storybook is first and foremost a library for displaying UI components. it's not really equipped to showcase non-UI elements like general utils and hooks. so while keeping this in mind, i think the best way to work around this is ensure stories are written in a way that illustrates what the utils do by creating some basic example code. we had a similar concept with storybook only components, but to lessen file clutter i think you can get away with something as simple as creating the example implementation directly in the stories file.
consider this example: here is a simple string reversal utility
// reverse.ts
const reverse = (str: string): string => {
return str.split("").reverse().join("");
};
export default reverse;
here is what a accompanying story could look like:
// reverse.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import reverse from "./reverse";
export interface ReverseProps {
text: string;
}
const ReverseDemo = (props: ReverseProps) => {
const { text } = props;
return (
<div>
<p>{reverse(text)}</p>
</div>
);
};
const meta: Meta<typeof ReverseDemo> = {
component: ReverseDemo,
tags: ["autodocs"],
};
export default meta;
type Story = StoryObj<typeof ReverseDemo>;
export const Default: Story = {
args: {
text: "a test string",
},
};
what we end up with is a simple story that displays the functionality of the utility while also allowing the user to interact with it directly via the args
the only caveat i can see with this approach is that there may be some manual documenting necessary to explain function parameters if the args
table isn't enough. not the end of the world, but something to keep in mind if further explanation is necessary.
core appears to do something similar in their hooks section.
as per our discussion earlier today: we should also take a look at moving storybook config from core
and into ibm-products
as well revisiting the config and preview template to see if we still need all the customization that is built into it. it may be that a lot of the stuff in there is dated and not necessary. we might be able to remove a bunch of that code in order to simplify our storybook config.
the overflow logic from TagOverflow
can easily be extracted into a reusable hook
import { useState, useEffect, useCallback, useRef } from 'react';
import { useResizeObserver } from '../../global/js/hooks/useResizeObserver';
const useOverflow = ({
items,
containingElementRef,
containerRef,
multiline,
measurementOffset,
overflowRef,
maxVisible,
onOverflowTagChange,
}) => {
const itemRefs = useRef<Map<string, string> | null>(null);
const itemOffset = 4;
const overflowIndicatorWidth = 40;
const resizeElm =
containingElementRef && containingElementRef.current
? containingElementRef
: containerRef;
const [containerWidth, setContainerWidth] = useState(0);
const [visibleItems, setVisibleItems] = useState([]);
const [overflowItems, setOverflowItems] = useState([]);
const handleResize = () => {
if (typeof resizeElm !== 'function' && resizeElm.current) {
setContainerWidth(resizeElm.current.offsetWidth);
}
};
useResizeObserver(resizeElm, handleResize);
const getMap = () => {
if (!itemRefs.current) {
itemRefs.current = new Map();
}
return itemRefs.current;
};
const itemRefHandler = (id, node) => {
const map = getMap();
if (id && node && map.get(id) !== node.offsetWidth) {
map.set(id, node.offsetWidth);
}
};
const getVisibleItems = useCallback(() => {
if (!itemRefs.current) {
return items;
}
if (multiline) {
const visibleItems = maxVisible ? items?.slice(0, maxVisible) : items;
return visibleItems;
}
const map = getMap();
const optionalContainingElement = containingElementRef?.current;
const measurementOffsetValue =
typeof measurementOffset === 'number' ? measurementOffset : 0;
const spaceAvailable = optionalContainingElement
? optionalContainingElement.offsetWidth - measurementOffsetValue
: containerWidth;
const overflowContainerWidth =
overflowRef &&
overflowRef.current &&
overflowRef.current.offsetWidth > overflowIndicatorWidth
? overflowRef.current.offsetWidth
: overflowIndicatorWidth;
const maxWidth = spaceAvailable - overflowContainerWidth;
let childrenWidth = 0;
let maxReached = false;
return items.reduce((prev, cur) => {
if (!maxReached) {
const itemWidth = (map ? Number(map.get(cur.id)) : 0) + itemOffset;
const fits = itemWidth + childrenWidth < maxWidth;
if (fits) {
childrenWidth += itemWidth;
prev.push(cur);
} else {
maxReached = true;
}
}
return prev;
}, []);
}, [
containerWidth,
containingElementRef,
items,
maxVisible,
measurementOffset,
multiline,
overflowRef,
]);
useEffect(() => {
let visibleItemsArr = getVisibleItems();
if (maxVisible && maxVisible < visibleItemsArr.length) {
visibleItemsArr = visibleItemsArr?.slice(0, maxVisible);
}
const hiddenItems = items?.slice(visibleItemsArr.length);
const overflowItemsArr = hiddenItems?.map(({ tagType, ...other }) => {
return { type: tagType, ...other };
});
setVisibleItems(visibleItemsArr);
setOverflowItems(overflowItemsArr);
onOverflowTagChange?.(overflowItemsArr);
}, [getVisibleItems, items, maxVisible, onOverflowTagChange]);
return {
visibleItems,
itemRefHandler,
overflowItems,
};
};
export default useOverflow;
...
const { visibleItems, overflowItems, itemRefHandler } = useOverflow({
items,
containingElementRef,
containerRef,
multiline,
measurementOffset,
overflowRef,
maxVisible,
onOverflowTagChange,
});
this logic could easily be retrofitted to work with;
ActionBar
DelimitedList
TruncatedList
TagOverflow
TagSet
StringFormatter
so basically a single hook could be used to satisfy the needs of all our various patterns and components that have some kind of interaction with overflow.
We are looking for a home for the two potential utilities on the product site which are currently listed as a pattern
Rather than cluttering the left hand nav, is it a good idea to have this only live in Storybook?