Skip to content

Commit

Permalink
Update menus in Dashboard body (#4285)
Browse files Browse the repository at this point in the history
* update menus in dashboard body to shad-cn

* update roles and labels to match tests

* remove errant text

* match test expectations

* update test

* fix placement of env variable declaration

* make aria labels more explicit

* fix tests

* remove comments

* rename folder

* dropdown icon spacing and figma adjustments

* rename component

* rever unintended changes

* prevent single selected item from being deselected

* update capitalization in text assertion

* explicitly set aria-disabled, remove checked binding

* tweak page locator, ignore pointer events on tooltip
  • Loading branch information
briangregoryholmes authored Mar 11, 2024
1 parent ec48c63 commit 1d1033e
Show file tree
Hide file tree
Showing 13 changed files with 331 additions and 92 deletions.
2 changes: 1 addition & 1 deletion web-common/src/components/button/Button.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
export let compact = false;
export let submitForm = false;
export let form = "";
export let label: string | undefined = undefined;
export let label: string | undefined | null = null;
export let square = false;
export let circle = false;
export let selected = false;
Expand Down
78 changes: 78 additions & 0 deletions web-common/src/components/menu/shadcn/SearchableMenu.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<script lang="ts">
import type { SearchableFilterSelectableItem } from "@rilldata/web-common/components/searchable-filter-menu/SearchableFilterSelectableItem";
import { fly } from "svelte/transition";
import TooltipContent from "../../tooltip/TooltipContent.svelte";
import SearchableMenuContent from "./SearchableMenuContent.svelte";
import Button from "../../button/Button.svelte";
import Tooltip from "@rilldata/web-common/components/tooltip/Tooltip.svelte";
import CaretDownIcon from "../../icons/CaretDownIcon.svelte";
import * as DropdownMenu from "@rilldata/web-common/components/dropdown-menu/";
export let selectableItems: SearchableFilterSelectableItem[];
export let selectedItems: boolean[];
export let tooltipText: string;
export let ariaLabel: string;
export let category: string;
export let disabled = false;
let active = false;
$: {
if (selectableItems?.length !== selectedItems?.length) {
throw new Error(
"SearchableFilterButton component requires props `selectableItems` and `selectedItems` to be arrays of equal length",
);
}
}
$: numAvailable = selectableItems?.length ?? 0;
$: numShown = selectedItems?.filter((x) => x).length ?? 0;
$: numShownString =
numAvailable === numShown ? "All" : `${numShown} of ${numAvailable}`;
</script>

<DropdownMenu.Root
closeOnItemClick={false}
typeahead={false}
bind:open={active}
>
<DropdownMenu.Trigger asChild let:builder>
<Tooltip
activeDelay={60}
alignment="start"
distance={8}
location="bottom"
suppress={active}
>
<Button builders={[builder]} type="text" label={ariaLabel} on:click>
<div
class="flex items-center gap-x-0.5 px-1.5 text-gray-700 hover:text-inherit"
>
<strong>{`${numShownString} ${category}`}</strong>
<span
class="transition-transform"
class:hidden={disabled}
class:-rotate-180={active}
>
<CaretDownIcon />
</span>
</div>
</Button>

<div slot="tooltip-content" transition:fly={{ duration: 300, y: 4 }}>
<TooltipContent maxWidth="400px">
{tooltipText}
</TooltipContent>
</div>
</Tooltip>
</DropdownMenu.Trigger>

<SearchableMenuContent
on:deselect-all
on:item-clicked
on:select-all
selectableGroups={[{ name: "", items: selectableItems }]}
selectedItems={[selectedItems]}
/>
</DropdownMenu.Root>
123 changes: 123 additions & 0 deletions web-common/src/components/menu/shadcn/SearchableMenuContent.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<script lang="ts">
import { getMenuGroups } from "@rilldata/web-common/components/searchable-filter-menu/SearchableFilterSelectableItem";
import type { SearchableFilterSelectableGroup } from "@rilldata/web-common/components/searchable-filter-menu/SearchableFilterSelectableItem";
import Search from "../../search/Search.svelte";
import Button from "../../button/Button.svelte";
import { createEventDispatcher } from "svelte";
import * as DropdownMenu from "@rilldata/web-common/components/dropdown-menu";
const dispatch = createEventDispatcher();
export let selectableGroups: SearchableFilterSelectableGroup[];
export let selectedItems: boolean[][];
export let allowMultiSelect = true;
let searchText = "";
$: menuGroups = getMenuGroups(selectableGroups, selectedItems, searchText);
$: numSelected = selectedItems.reduce(
(sel, items) => sel + items.filter((i) => i).length,
0,
);
$: singleSelection = numSelected === 1;
$: numSelectedNotShown =
numSelected -
menuGroups.reduce(
(sel, mg) => sel + mg.items.filter((i) => i.selected).length,
0,
);
$: selectableCount = selectableGroups.reduce(
(sel, g) => sel + g.items.length,
0,
);
$: searchResultCount = menuGroups.reduce(
(sel, mg) => sel + mg.items.length,
0,
);
$: allToggleText =
numSelected === selectableCount ? "Deselect all" : "Select all";
$: allToggleEvt =
numSelected === selectableCount ? "deselect-all" : "select-all";
$: dispatchAllToggleEvt = () => {
dispatch(allToggleEvt);
};
</script>

<DropdownMenu.Content
align="start"
class="max-w-96 max-h-80 min-w-60 p-0 overflow-hidden flex flex-col"
>
<div class="px-3 pt-3">
<Search bind:value={searchText} showBorderOnFocus={false} />
</div>

<div class="overflow-y-scroll overflow-x-hidden size-full py-1">
{#if searchResultCount > 0}
{#each menuGroups as { name, items, showDivider }}
{#if items.length}
{#if showDivider}
<DropdownMenu.Separator />
{/if}
{#if name}
<span class="gap-x-3 px-3 pb-1 text-gray-500 font-semibold">
{name}
</span>
{/if}

<DropdownMenu.Group class="px-1">
{#each items as { name, label, selected, index }}
<svelte:component
this={allowMultiSelect
? DropdownMenu.CheckboxItem
: DropdownMenu.Item}
{...allowMultiSelect ? { checked: selected } : {}}
class="text-xs cursor-pointer"
role="menuitem"
disabled={singleSelection && selected}
aria-disabled={singleSelection && selected}
on:click={() => {
if (singleSelection && selected) return;

dispatch("item-clicked", { index, name });
}}
>
<span class:ui-copy-disabled={!selected && allowMultiSelect}>
{#if label.length > 240}
{label.slice(0, 240)}...
{:else}
{label}
{/if}
</span>
</svelte:component>
{/each}
</DropdownMenu.Group>
{/if}
{/each}
{:else}
<div class="ui-copy-disabled text-center p-2 w-full">no results</div>
{/if}
</div>

{#if allowMultiSelect}
<DropdownMenu.Group
class="flex items-center justify-between pl-2 pr-4 py-1 border-t bg-gray-50 dark:bg-gray-600 border-gray-200 dark:border-gray-500"
>
<Button compact on:click={dispatchAllToggleEvt} type="text">
{allToggleText}
</Button>
{#if numSelectedNotShown}
<div class="ui-label">
{numSelectedNotShown} other value{numSelectedNotShown > 1 ? "s" : ""} selected
</div>
{/if}
</DropdownMenu.Group>
{/if}
</DropdownMenu.Content>
42 changes: 42 additions & 0 deletions web-common/src/components/menu/shadcn/SelectMenu.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<script lang="ts">
import type { SelectMenuItem } from "../types";
import Button from "../../button/Button.svelte";
import CaretDownIcon from "../../icons/CaretDownIcon.svelte";
import * as DropdownMenu from "@rilldata/web-common/components/dropdown-menu";
import SelectMenuContent from "./SelectMenuContent.svelte";
export let options: SelectMenuItem[];
export let selections: Array<string | number>;
// this is fixed text that will always be displayed in the button
export let fixedText = "";
export let disabled = false;
export let active = false;
export let ariaLabel: string;
$: firstSelectedKey = selections?.[0] ?? null;
$: firstSelection = firstSelectedKey
? options.find((option) => option.key === firstSelectedKey)
: null;
</script>

<DropdownMenu.Root bind:open={active}>
<DropdownMenu.Trigger asChild let:builder>
<Button builders={[builder]} type="text" label={ariaLabel} on:click>
<div
class="flex items-center gap-x-0.5 px-1.5 text-gray-700 hover:text-inherit"
>
<p class="truncate">{fixedText} <b>{firstSelection?.main}</b></p>
<span
class="transition-transform"
class:hidden={disabled}
class:-rotate-180={active}
>
<CaretDownIcon />
</span>
</div>
</Button>
</DropdownMenu.Trigger>

<SelectMenuContent {options} {selections} on:select />
</DropdownMenu.Root>
47 changes: 47 additions & 0 deletions web-common/src/components/menu/shadcn/SelectMenuContent.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import type { SelectMenuItem } from "../types";
import * as DropdownMenu from "@rilldata/web-common/components/dropdown-menu";
export let options: SelectMenuItem[];
export let selections: Array<string | number>;
const dispatch = createEventDispatcher();
function handleClick(index: number) {
// if (!multiSelect) selection = event.detail;
const selection = options?.[index];
if (!options) return;
dispatch("select", selection);
}
</script>

<DropdownMenu.Content class="min-w-44" align="start">
{#each options as option, i (option.key)}
{@const selected = selections.includes(option.key)}
<DropdownMenu.CheckboxItem
role="menuitem"
disabled={option.disabled}
class="text-xs cursor-pointer rounded-none"
checked={selected}
on:click={() => handleClick(i)}
>
<div class="flex flex-col">
<div class:text-gray-400={option.disabled} class:font-bold={selected}>
{option.main}
</div>
{#if option.description}
<p class="ui-copy-muted" style:font-size="11px">
{option.description}
</p>
{/if}
</div>
{option.right || ""}
</DropdownMenu.CheckboxItem>
{#if option.divider}
<DropdownMenu.Separator />
{/if}
{/each}
</DropdownMenu.Content>
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,7 @@ props as needed.
-->
<script lang="ts">
import type { SearchableFilterSelectableItem } from "@rilldata/web-common/components/searchable-filter-menu/SearchableFilterSelectableItem";
import { fly } from "svelte/transition";
import WithTogglableFloatingElement from "../floating-element/WithTogglableFloatingElement.svelte";
import SelectButton from "../menu/triggers/SelectButton.svelte";
import Tooltip from "../tooltip/Tooltip.svelte";
import TooltipContent from "../tooltip/TooltipContent.svelte";
import SearchableFilterDropdown from "./SearchableFilterDropdown.svelte";
import SearchableMenu from "../menu/shadcn/SearchableMenu.svelte";
export let selectableItems: SearchableFilterSelectableItem[];
export let selectedItems: boolean[];
Expand All @@ -47,47 +41,17 @@ props as needed.
);
}
}
let active = false;
$: numAvailable = selectableItems?.length ?? 0;
$: numShown = selectedItems?.filter((x) => x).length ?? 0;
$: numShownString =
numAvailable === numShown ? "All" : `${numShown} of ${numAvailable}`;
</script>

<WithTogglableFloatingElement
alignment="start"
bind:active
distance={8}
let:toggleFloatingElement
>
<Tooltip
activeDelay={60}
alignment="start"
distance={8}
location="bottom"
suppress={active}
>
<SelectButton label={tooltipText} {active} on:click={toggleFloatingElement}
><strong>{numShownString} {label}</strong></SelectButton
>
<div slot="tooltip-content" transition:fly={{ duration: 300, y: 4 }}>
<TooltipContent maxWidth="400px">
{tooltipText}
</TooltipContent>
</div>
</Tooltip>
<SearchableFilterDropdown
let:toggleFloatingElement
on:apply
on:click-outside={toggleFloatingElement}
on:deselect-all
on:escape={toggleFloatingElement}
on:item-clicked
on:search
on:select-all
selectableGroups={[{ name: "", items: selectableItems }]}
selectedItems={[selectedItems]}
slot="floating-element"
/>
</WithTogglableFloatingElement>
<SearchableMenu
on:apply
on:deselect-all
on:item-clicked
on:search
on:select-all
ariaLabel={tooltipText}
category={label}
bind:selectedItems
{selectableItems}
{tooltipText}
/>
2 changes: 1 addition & 1 deletion web-common/src/components/tooltip/TooltipTitle.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<div
class="grid gap-x-2 items-center pt-1 pb-1 items-baseline"
class="grid gap-x-2 pointer-events-none pt-1 pb-1 items-baseline"
style="grid-template-columns: auto max-content"
style:min-width="200px"
>
Expand Down
Loading

1 comment on commit 1d1033e

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.