Skip to content

Commit ae55066

Browse files
committed
Dropdown: single divider for add-label + View-all expands inline
Labels demo: drop the extra divider line above the Add-label row so a single divider (the sticky search's own bottom rule) sits between the search field and the new-label item, per designer correction (Figma 64-626). Filters demo: a viewAll section now previews its first rows and the View-all toggle reveals the remaining rows inline (becoming Show less), instead of being a styled no-op. Keyboard-accessible (menuitem, activated via Enter in the play test) with aria-expanded reflecting the state. Closes #31
1 parent 77204e2 commit ae55066

1 file changed

Lines changed: 56 additions & 21 deletions

File tree

packages/propel/src/components/dropdown/dropdown.stories.tsx

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ export const Status: Story = {
323323
* Demo 2 — **Labels**. Multi-select: each row is a `DropdownCheckboxItem` (the propel
324324
* `Checkbox` as the leading control) plus a color swatch, with a search header. When
325325
* the typed query has no exact match, an "Add label" option (Figma `64-626`) appears,
326-
* separated from the search by a double divider line.
326+
* separated from the search by a single divider line.
327327
*/
328328
export const Labels: Story = {
329329
render: function LabelsStory() {
@@ -344,18 +344,15 @@ export const Labels: Story = {
344344
search={<DropdownSearch value={query} onValueChange={setQuery} />}
345345
>
346346
{canAdd ? (
347-
<>
348-
{/* Two horizontal divider lines between the search and the new-label
349-
item (Figma 64-626): the sticky search already draws a bottom rule;
350-
this adds the second one directly above the add-label row. */}
351-
<div className="-mx-1 mb-1 border-t border-subtle" />
352-
<DropdownItem
353-
variant="default"
354-
icon={<Plus className="text-icon-secondary" />}
355-
label={`Add label "${trimmed}"`}
356-
closeOnClick={false}
357-
/>
358-
</>
347+
// A single horizontal divider separates the search from the new-label item
348+
// (Figma 64-626): the sticky DropdownSearch already draws its own bottom
349+
// rule, so the "Add label" row mounts directly beneath it with no extra line.
350+
<DropdownItem
351+
variant="default"
352+
icon={<Plus className="text-icon-secondary" />}
353+
label={`Add label "${trimmed}"`}
354+
closeOnClick={false}
355+
/>
359356
) : null}
360357
{visible.map((l) => (
361358
<DropdownCheckboxItem
@@ -656,6 +653,10 @@ export const Priority: Story = {
656653
},
657654
};
658655

656+
// How many rows a `viewAll` section shows before the "View all" toggle. The rest stay
657+
// collapsed inline until the toggle is activated.
658+
const VIEW_ALL_PREVIEW = 2;
659+
659660
/**
660661
* Regression — **CheckedFillVisible**. Locks in the fix for the invisible
661662
* checked checkbox: a tone-less `CheckboxVisual` (as rendered by
@@ -729,13 +730,15 @@ export const CheckedFillVisible: Story = {
729730
* Demo 8 — **Filters**. Multi-select across several titled, collapsible sections
730731
* (Priority, State, Assignee, …). Each item carries a leading icon; a chevron on the
731732
* heading collapses/expands the category; categories are separated by a divider; and
732-
* the "View all" link sits in the heading's trailing slot (no hover background,
733-
* `cursor-pointer`).
733+
* a long category previews only its first few rows with a "View all" toggle that
734+
* expands the remaining rows inline (and collapses back to "Show less").
734735
*/
735736
export const Filters: Story = {
736737
render: function FiltersStory() {
737738
const [checked, setChecked] = React.useState<Record<string, boolean>>({});
738739
const [collapsed, setCollapsed] = React.useState<Record<string, boolean>>({});
740+
// Which `viewAll` sections have been expanded to show every row.
741+
const [expandedAll, setExpandedAll] = React.useState<Record<string, boolean>>({});
739742
const [query, setQuery] = React.useState("");
740743
const toggle = (key: string) => (next: boolean) => setChecked((c) => ({ ...c, [key]: next }));
741744
const match = (label: string) => label.toLowerCase().includes(query.toLowerCase());
@@ -771,6 +774,12 @@ export const Filters: Story = {
771774
const items = section.items.filter((i) => match(i.label));
772775
if (items.length === 0) return null;
773776
const isCollapsed = Boolean(collapsed[section.title]);
777+
const isExpandedAll = Boolean(expandedAll[section.title]);
778+
// A `viewAll` section previews only its first rows until expanded; the
779+
// toggle appears only when there are more rows than the preview shows.
780+
const hasOverflow = Boolean(section.viewAll) && items.length > VIEW_ALL_PREVIEW;
781+
const visibleItems =
782+
hasOverflow && !isExpandedAll ? items.slice(0, VIEW_ALL_PREVIEW) : items;
774783
return (
775784
<React.Fragment key={section.title}>
776785
{/* Divider between categories. */}
@@ -794,7 +803,7 @@ export const Filters: Story = {
794803
onClick={() => setCollapsed((c) => ({ ...c, [section.title]: !isCollapsed }))}
795804
/>
796805
{!isCollapsed
797-
? items.map((i) => (
806+
? visibleItems.map((i) => (
798807
<DropdownCheckboxItem
799808
key={i.key}
800809
icon={i.icon}
@@ -804,14 +813,24 @@ export const Filters: Story = {
804813
/>
805814
))
806815
: null}
807-
{/* "View all" is its own menuitem with link emphasis: no hover
808-
background + cursor-pointer. */}
809-
{!isCollapsed && section.viewAll ? (
816+
{/* "View all" is its own menuitem (keyboard-focusable like any row)
817+
with link emphasis: no hover background + cursor-pointer. Clicking
818+
it reveals the remaining rows inline; once expanded it becomes
819+
"Show less". `aria-expanded` reflects the inline-expansion state. */}
820+
{!isCollapsed && hasOverflow ? (
810821
<DropdownItem
811822
variant="default"
812823
emphasis="link"
813-
label={<span className="text-accent-primary">View all</span>}
824+
aria-expanded={isExpandedAll}
825+
label={
826+
<span className="text-accent-primary">
827+
{isExpandedAll ? "Show less" : `View all (${items.length})`}
828+
</span>
829+
}
814830
closeOnClick={false}
831+
onClick={() =>
832+
setExpandedAll((s) => ({ ...s, [section.title]: !isExpandedAll }))
833+
}
815834
/>
816835
) : null}
817836
</DropdownGroup>
@@ -827,7 +846,23 @@ export const Filters: Story = {
827846
await openMenu(canvas, "Filters");
828847
await waitFor(() => expect(findItem("menuitemcheckbox", "Urgent")).toBeDefined());
829848
await expect(findItem("menuitemcheckbox", "Backlog")).toBeDefined();
830-
await expect(findItem("menuitemcheckbox", "David Wilson")).toBeDefined();
849+
// The Assignee section previews only its first rows; later assignees are hidden
850+
// behind "View all" until it is activated.
851+
await expect(findItem("menuitemcheckbox", "Amelia Parker")).toBeDefined();
852+
await expect(findItem("menuitemcheckbox", "Ethan Parker")).toBeUndefined();
853+
});
854+
await step("View all expands the remaining rows inline", async () => {
855+
const viewAll = (await waitFor(() => findItem("menuitem", "View all"))) as HTMLElement;
856+
await expect(viewAll).toHaveAttribute("aria-expanded", "false");
857+
// Keyboard-accessible: activate the menuitem with Enter rather than a pointer.
858+
viewAll.focus();
859+
await userEvent.keyboard("{Enter}");
860+
await waitFor(() => expect(findItem("menuitemcheckbox", "Ethan Parker")).toBeDefined());
861+
// The toggle now offers to collapse again.
862+
const showLess = findItem("menuitem", "Show less") as HTMLElement;
863+
await expect(showLess).toHaveAttribute("aria-expanded", "true");
864+
await userEvent.click(showLess);
865+
await waitFor(() => expect(findItem("menuitemcheckbox", "Ethan Parker")).toBeUndefined());
831866
});
832867
await step("collapse a category from its heading", async () => {
833868
const heading = (await waitFor(() =>

0 commit comments

Comments
 (0)