Skip to content

Commit

Permalink
- Moved generic list styles to the RawList component
Browse files Browse the repository at this point in the history
- Divided the list into two type, selectable and dropdown.
  • Loading branch information
duduBTW committed Nov 2, 2024
1 parent 2398638 commit 7cff4b7
Show file tree
Hide file tree
Showing 10 changed files with 182 additions and 65 deletions.
65 changes: 65 additions & 0 deletions src/renderer/src/components/dropdown-list/DropdownList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import DropdownListItem from "./DropdownListItem";
import { useRovingFocusGroup } from "@renderer/lib/roving-focus-group/rovingFocusGroup";
import { createContext, createSignal, JSX, ParentComponent, useContext } from "solid-js";
import { RawList } from "../raw-list/RawList";
import { TokenNamespace } from "@renderer/lib/tungsten/token";

export type Props = JSX.IntrinsicElements["div"];

export type Context = ReturnType<typeof useProviderValue>;
function useProviderValue() {
const namespace = new TokenNamespace();
let pointerLeaveTimeout: NodeJS.Timeout | undefined;

const [isHighlighted, setIsHighlighted] = createSignal(false);

const rovingFocusGroup = useRovingFocusGroup({
updateFocusOnHover: true,
onKeyUp: () => {
setIsHighlighted(true);
},
});

const handleItemPointerLeave = () => {
clearTimeout(pointerLeaveTimeout);
pointerLeaveTimeout = setTimeout(() => {
setIsHighlighted(false);
}, 60);
};

const handleItemPointerMove = () => {
setIsHighlighted(true);
};

return {
...rovingFocusGroup,
handleItemPointerMove,
isHighlighted,
handleItemPointerLeave,
namespace,
};
}

export const DropdownListContext = createContext<Context>();
const DropdownListRoot: ParentComponent<Props> = (props) => {
const value = useProviderValue();
return (
<DropdownListContext.Provider value={value}>
<RawList {...props} {...value.attrs} />
</DropdownListContext.Provider>
);
};

export function useDropdownList(): Context {
const state = useContext(DropdownListContext);
if (!state) {
throw new Error("useDropdownList needs to be used inisde of the `ListContext` component.");
}
return state;
}

const DropdownList = Object.assign(DropdownListRoot, {
Item: DropdownListItem,
});

export default DropdownList;
31 changes: 31 additions & 0 deletions src/renderer/src/components/dropdown-list/DropdownListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useDropdownList } from "./DropdownList";
import { Component, onCleanup } from "solid-js";
import { JSX } from "solid-js/jsx-runtime";
import { RawList } from "../raw-list/RawList";

export type Props = JSX.IntrinsicElements["button"];
const DropdownListItem: Component<Props> = (props) => {
const state = useDropdownList();
const value = state.namespace.create();
const { attrs, tabIndex, isSelected } = state.item(value, {
onPointerMove: state.handleItemPointerMove,
});

onCleanup(() => {
state.namespace.destroy(value);
});

return (
<RawList.Item
onPointerLeave={state.handleItemPointerLeave}
tabIndex={tabIndex()}
classList={{
"bg-overlay/30": state.isHighlighted() && isSelected(),
}}
{...attrs}
{...props}
/>
);
};

export default DropdownListItem;
24 changes: 24 additions & 0 deletions src/renderer/src/components/raw-list/RawList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { cn } from "@renderer/lib/css.utils";
import { Component, JSX, splitProps } from "solid-js";

export const RawListContainer: Component<JSX.IntrinsicElements["div"]> = (_props) => {
const [props, rest] = splitProps(_props, ["class"]);
return <div class={cn("flex flex-col gap-0.5", props.class)} {...rest} />;
};
export const RawListItem: Component<JSX.IntrinsicElements["button"]> = (_props) => {
const [props, rest] = splitProps(_props, ["class"]);
return (
<button
class={cn(
"flex items-center justify-between rounded-md px-2 py-1.5 disabled:opacity-50 disabled:pointer-events-none",
props.class,
)}
{...rest}
/>
);
};

/** List component with only styles */
export const RawList = Object.assign(RawListContainer, {
Item: RawListItem,
});
8 changes: 4 additions & 4 deletions src/renderer/src/components/select/Select.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Component, createMemo, JSX, ParentComponent } from "solid-js";
import Popover, { Props as PopoverProps, usePopover } from "../popover/Popover";
import { ChevronsUpDownIcon } from "lucide-solid";
import List, { Props as ListProps } from "../list/List";
import ListItem, { Props as ListItemProps } from "../list/ListItem";
import SelectableList, { Props as ListProps } from "../selectable-list/SelectableList";
import SelectableListItem, { Props as ListItemProps } from "../selectable-list/SelectableListItem";

export const SelectContainer: ParentComponent<PopoverProps> = (props) => {
return (
Expand Down Expand Up @@ -47,13 +47,13 @@ export const SelectContent: Component<ListProps> = (props) => {
width: width(),
}}
>
<List {...props} />
<SelectableList {...props} />
</Popover.Content>
</Popover.Portal>
);
};
export const SelectOption: Component<ListItemProps> = (props) => {
return <ListItem {...props} />;
return <SelectableListItem {...props} />;
};

const Select = Object.assign(SelectContainer, {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { cn } from "@renderer/lib/css.utils";
import ListItem from "./ListItem";
import SelectableListItem from "./SelectableListItem";
import { useRovingFocusGroup } from "@renderer/lib/roving-focus-group/rovingFocusGroup";
import { Accessor, createContext, JSX, ParentComponent, splitProps, useContext } from "solid-js";
import useControllableState from "@renderer/lib/controllable-state";
import { RawList } from "../raw-list/RawList";

const DEFAULT_SELECTED_VALUE = "";

export type ListOptions = {
export type SelectableListOptions = {
defaultValue?: string;
value?: Accessor<string>;
onValueChange?: (newValue: string) => void;
};

export type Props = JSX.IntrinsicElements["div"] & ListOptions;
export type Props = JSX.IntrinsicElements["div"] & SelectableListOptions;

export type Context = ReturnType<typeof useProviderValue>;
function useProviderValue(props: ListOptions) {
function useProviderValue(props: SelectableListOptions) {
const rovingFocusGroup = useRovingFocusGroup({
updateFocusOnHover: true,
defaultProp: props.value?.() ?? props.defaultValue,
Expand All @@ -33,35 +33,29 @@ function useProviderValue(props: ListOptions) {
};
}

export const ListContext = createContext<Context>();
const ListRoot: ParentComponent<Props> = (_props) => {
const [props, rest] = splitProps(_props, [
"value",
"onValueChange",
"defaultValue",
"children",
"class",
]);
export const SelectableListContext = createContext<Context>();
const SelectableListRoot: ParentComponent<Props> = (_props) => {
const [props, rest] = splitProps(_props, ["value", "onValueChange", "defaultValue", "children"]);
const value = useProviderValue(props);
return (
<ListContext.Provider value={value}>
<div {...rest} {...value.attrs} class={cn("flex flex-col gap-0.5", props.class)}>
<SelectableListContext.Provider value={value}>
<RawList {...rest} {...value.attrs}>
{props.children}
</div>
</ListContext.Provider>
</RawList>
</SelectableListContext.Provider>
);
};

export function useList(): Context {
const state = useContext(ListContext);
export function useSelectableList(): Context {
const state = useContext(SelectableListContext);
if (!state) {
throw new Error("useList needs to be used inisde of the `ListContext` component.");
throw new Error("useSelectableList needs to be used inisde of the `ListContext` component.");
}
return state;
}

const List = Object.assign(ListRoot, {
Item: ListItem,
const SelectableList = Object.assign(SelectableListRoot, {
Item: SelectableListItem,
});

export default List;
export default SelectableList;
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
import { cn } from "@renderer/lib/css.utils";
import { useList } from "./List";
import { useSelectableList } from "./SelectableList";
import { Component, createMemo, Show, splitProps } from "solid-js";
import { JSX } from "solid-js/jsx-runtime";
import { CheckIcon } from "lucide-solid";
import { RawList } from "../raw-list/RawList";

export type Props = JSX.IntrinsicElements["button"] & {
value?: string;
value: string;
onSelectedByClick?: () => void;
};
const ListItem: Component<Props> = (_props) => {
const [props, rest] = splitProps(_props, ["value", "onSelectedByClick", "class", "children"]);
const SelectableListItem: Component<Props> = (_props) => {
const [props, rest] = splitProps(_props, ["value", "onSelectedByClick", "children"]);

const value = () => {
return props.value ?? Math.random().toString(36);
};

const state = useList();
const state = useSelectableList();
const {
attrs,
isSelected: isFocused,
tabIndex,
} = state.item(value(), {
} = state.item(props.value, {
onSelectedByClick: () => {
props.onSelectedByClick?.();
if (!props.value) {
Expand All @@ -36,23 +32,20 @@ const ListItem: Component<Props> = (_props) => {
});

return (
<button
class={cn(
"flex items-center justify-between rounded-md px-2 py-1.5 hover:bg-stroke data-[selected=true]:bg-overlay/30 disabled:opacity-50 disabled:pointer-events-none",
props.class,
)}
<RawList.Item
tabIndex={tabIndex()}
data-selected={isFocused()}
classList={{
"bg-overlay/30": isFocused(),
}}
{...attrs}
{...rest}
>
{props.children}

<Show when={isSelected()}>
<CheckIcon size={14} />
</Show>
</button>
</RawList.Item>
);
};

export default ListItem;
export default SelectableListItem;
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import List from "@renderer/components/list/List";
import SelectableList from "@renderer/components/selectable-list/SelectableList";
import { SortAsc, SortDesc } from "lucide-solid";
import { Component, createMemo, createSignal, For, Match, Setter, Switch } from "solid-js";
import { OrderDirection, OrderOptions, Order } from "src/@types";
Expand Down Expand Up @@ -87,7 +87,7 @@ const SongListSearchOrderBy: Component<OrderSelectProps> = (props) => {
</FilterOption.List>

<FilterOption.Content class="w-48">
<List
<SelectableList
onValueChange={(newSelectedOption) => {
setOption(newSelectedOption as OrderOptions);
handlerOrderChanged();
Expand All @@ -96,12 +96,12 @@ const SongListSearchOrderBy: Component<OrderSelectProps> = (props) => {
>
<For each={orderOptions}>
{(option) => (
<List.Item onSelectedByClick={() => setIsOpen(false)} value={option.value}>
<SelectableList.Item onSelectedByClick={() => setIsOpen(false)} value={option.value}>
{option.text}
</List.Item>
</SelectableList.Item>
)}
</For>
</List>
</SelectableList>
</FilterOption.Content>
</FilterOption>
);
Expand Down
14 changes: 9 additions & 5 deletions src/renderer/src/components/song/song-list/SongList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import List from "@renderer/components/list/List";
import { Optional, Order, ResourceID, SongsQueryPayload, Tag } from "../../../../../@types";
import { SearchQueryError } from "../../../../../main/lib/search-parser/@search-types";
import { namespace } from "../../../App";
Expand All @@ -10,6 +9,7 @@ import SongListSearch from "../song-list-search/SongListSearch";
import { songsSearch } from "./song-list.utils";
import { Component, createEffect, createSignal, onCleanup, onMount } from "solid-js";
import { ListPlus } from "lucide-solid";
import DropdownList from "@renderer/components/dropdown-list/DropdownList";

export type SongViewProps = {
isAllSongs?: boolean;
Expand Down Expand Up @@ -105,12 +105,16 @@ const SongList: Component<SongViewProps> = (props) => {

const SongListContextMenuContent: Component = () => {
return (
<List class="w-40">
<List.Item>
<DropdownList class="w-40">
<DropdownList.Item>
<span>Add to Playlist</span>
<ListPlus class="text-subtext" size={20} />
</List.Item>
</List>
</DropdownList.Item>
<DropdownList.Item>
<span>Play next</span>
<ListPlus class="text-subtext" size={20} />
</DropdownList.Item>
</DropdownList>
);
};

Expand Down
14 changes: 7 additions & 7 deletions src/renderer/src/components/song/song-queue/SongQueue.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import List from "@renderer/components/list/List";
import { Song } from "../../../../../@types";
import { namespace } from "../../../App";
import Impulse from "../../../lib/Impulse";
Expand All @@ -7,6 +6,7 @@ import InfiniteScroller from "../../InfiniteScroller";
import SongItem from "../song-item/SongItem";
import { Component, createSignal, onCleanup, onMount } from "solid-js";
import { DeleteIcon, ListPlus } from "lucide-solid";
import DropdownList from "@renderer/components/dropdown-list/DropdownList";

const SongQueue: Component = () => {
const [count, setCount] = createSignal(0);
Expand Down Expand Up @@ -111,19 +111,19 @@ const SongQueue: Component = () => {
type QueueContextMenuContentProps = { song: Song };
const QueueContextMenuContent: Component<QueueContextMenuContentProps> = (props) => {
return (
<List class="w-52">
<List.Item>
<DropdownList class="w-52">
<DropdownList.Item>
<span>Add to Playlist</span>
<ListPlus class="text-subtext" size={20} />
</List.Item>
<List.Item
</DropdownList.Item>
<DropdownList.Item
onClick={() => window.api.request("queue::removeSong", props.song.path)}
class="text-red"
>
<span>Remove from queue</span>
<DeleteIcon class="opacity-80" size={20} />
</List.Item>
</List>
</DropdownList.Item>
</DropdownList>
);
};

Expand Down
Loading

0 comments on commit 7cff4b7

Please sign in to comment.