Skip to content

Commit

Permalink
feat: SelectMenu component
Browse files Browse the repository at this point in the history
  • Loading branch information
Bob Fanger committed Apr 16, 2023
1 parent 01d84de commit e108d0a
Show file tree
Hide file tree
Showing 6 changed files with 363 additions and 3 deletions.
42 changes: 42 additions & 0 deletions packages/blender-elements/src/SelectMenu/Option.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<script lang="ts">
export let value: string;
export let icon: string | undefined = undefined;
export let label: string | undefined = undefined;
</script>

<button class="option" on:click>
{#if icon}
<span class="icon" style="background-image: var(--icon-{icon})" />
{/if}
<span>{label ?? value}</span>
</button>

<style lang="scss">
.option {
font: 12px system-ui, sans-serif;
color: #dddddd;
padding: 0 6px 0 6px;
user-select: none;
background: transparent;
border: 0;
border-radius: 3px;
text-align: left;
display: flex;
align-items: center;
gap: 4px;
min-height: 18px;
cursor: pointer;
&:hover {
background: #4772b3;
color: #ffffff;
}
}
.icon {
display: inline-block;
width: 16px;
height: 16px;
background: no-repeat center center;
background-size: contain;
}
</style>
24 changes: 24 additions & 0 deletions packages/blender-elements/src/SelectMenu/SelectMenu.story.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script lang="ts">
import Base from "../Base.svelte";
import SelectMenu from "./SelectMenu.svelte";
export let Hst: any;
let value = "Both";
const options = ["None", "Back", "Front", "Both"];
</script>

<Hst.Story>
<Base>
<div style="max-width: 200px">
<SelectMenu legend="Fill Mode" bind:value {options} />
</div>
</Base>
<svelte:fragment slot="controls">
<Hst.Select
bind:value
title="value"
options={["None", "Back", "Front", "Both"]}
/>
</svelte:fragment>
</Hst.Story>
221 changes: 221 additions & 0 deletions packages/blender-elements/src/SelectMenu/SelectMenu.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
<script lang="ts">
import { createEventDispatcher, getContext, setContext, tick } from "svelte";
import Option from "./Option.svelte";
type OptionType = string | { value: string; label?: string; icon?: string };
export let value: string;
export let options: OptionType[];
export let legend = "";
$: option = options.find((entry) => {
if (typeof entry === "string") {
return entry === value;
}
return entry.value === value;
});
const dispatch = createEventDispatcher();
let el: HTMLDivElement;
let expanded: undefined | { x: "LEFT" | "RIGHT"; y: "UP" | "DOWN" };
let timer: number | undefined;
function select(next: OptionType) {
value = typeof next === "string" ? next : next.value;
dispatch("change", value);
collapse();
}
function expand() {
expanded = {
x: "LEFT",
y: "DOWN",
};
tick().then(() => {
if (!expanded) {
return;
}
const bounds = el.getBoundingClientRect();
const { x, y, width, height } = bounds;
const { innerWidth, innerHeight } = window;
if (x < 0) {
expanded.x = "RIGHT";
}
if (y + height > innerHeight) {
expanded.y = "UP";
}
});
}
function collapse() {
expanded = undefined;
}
function onLeave() {
timer = window.setTimeout(collapse, 0);
}
function onEnter() {
clearTimeout(timer);
timer = undefined;
}
</script>

<div
class="search-field"
class:expanded
class:up={expanded?.y === "UP"}
class:right={expanded?.x === "RIGHT"}
class:down={expanded?.y === "DOWN"}
class:left={expanded?.x === "LEFT"}
>
<button class="value" on:click={expand}>
{#if typeof option === "string"}
<span>{option}</span>
{:else if option}
{#if option.icon}
<span
class="icon"
style="background-image: var(--icon-{option.icon})"
/>
{/if}
<span>{option.label ?? option.value}</span>
{/if}
</button>
{#if expanded}
<div class="popout" bind:this={el}>
<button class="detector" on:mouseleave={onLeave} on:click={collapse} />
<div class="options" on:mouseenter={onEnter}>
{#each options as option}
{#if typeof option === "string"}
<Option value={option} on:click={() => select(option)} />
{:else}
<Option
value={option.value}
icon={option.icon}
label={option.label}
on:click={() => select(option)}
/>
{/if}
{/each}
</div>
{#if legend}
<div class="legend" on:mouseenter={onEnter}>{legend}</div>
{/if}
</div>
{/if}
</div>

<style lang="scss">
.search-field {
position: relative;
}
.value {
font: 12px system-ui, sans-serif;
appearance: none;
background-color: transparent;
border: none;
box-sizing: border-box;
width: 100%;
color: #fdfdfd;
outline: none;
display: flex;
align-items: center;
gap: 4px;
padding: 0 20px 0 4px;
background: #1d1d1d;
border: 1px solid #3d3d3d;
box-shadow: 0 1px 3px rgba(black, 0.3);
border-radius: 4px;
text-align: left;
min-height: 18px;
cursor: pointer;
&:hover {
background: #232323;
border-color: #414141;
}
&:after {
content: "";
position: absolute;
top: 1px;
right: 3px;
width: 16px;
height: 16px;
background: var(--icon-chevron-down) center center no-repeat;
opacity: 0.5;
}
.expanded & {
background: #446290;
color: #ffffff;
}
.expanded.up & {
border-top-left-radius: 0;
border-top-right-radius: 0;
border-top-color: #446290;
}
.expanded.down & {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
border-bottom-color: #446290;
}
}
.icon {
display: inline-block;
width: 16px;
height: 16px;
background: no-repeat center center;
background-size: contain;
}
.popout {
position: absolute;
z-index: 1;
box-sizing: border-box;
min-width: 100%;
background: #181818;
border: 1px solid #242424;
border-radius: 4px;
.up & {
border-top-left-radius: 0;
border-top-right-radius: 0;
bottom: 100%;
}
.right & {
left: 0;
}
.down & {
top: 100%;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
.left & {
right: 0;
}
}
.detector {
position: absolute;
top: -32px;
right: -32px;
bottom: -32px;
left: -32px;
background-color: transparent;
border: none;
}
.options {
position: relative;
display: flex;
flex-direction: column;
gap: 4px;
padding: 2px;
}
.legend {
position: relative;
color: #989898;
padding: 5px 8px 4px 8px;
.down & {
border-top: 1px solid #2f2f2f;
}
}
</style>
Loading

0 comments on commit e108d0a

Please sign in to comment.