Skip to content

Commit

Permalink
Merge pull request #88 from UrbanInstitute/patch-dropdown-variant
Browse files Browse the repository at this point in the history
Add style variants, visible label, and updated story controls to BasicDropdown
  • Loading branch information
benkates authored May 13, 2024
2 parents cddb11d + 55d0db0 commit 21a1227
Show file tree
Hide file tree
Showing 5 changed files with 250 additions and 75 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Next

- Update BasicDropdown with secondary style variants, a visible label, and better story controls
- Add LoadingWrapper component to provide a loading graphic for components that need it

## v0.8.0
Expand Down
135 changes: 102 additions & 33 deletions src/lib/BasicDropdown/BasicDropdown.stories.svelte
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
<script context="module">
import BasicDropdown from "./BasicDropdown.svelte";
import IconDownload from "$lib/Button/IconDownload.svelte";
import urbanColors from "$lib/utils/urbanColors.js";
export const meta = {
title: "Components/BasicDropdown",
description: "A basic dropdown component that uses a <select> element under the hood.",
component: BasicDropdown,
tags: ["autodocs"],
argTypes: {
arrowFillColor: { control: "color" },
data: { control: "object" }
variant: {
options: ["primary", "secondary-blue", "secondary-black", "secondary-yellow"],
control: { type: "select" }
},
data: { control: "object" },
showLabel: { control: "boolean" },
value: { control: "text" },
placeholder: { control: "text" }
},
parameters: {
docs: {
Expand All @@ -25,66 +33,127 @@
import { fireEvent, within, expect } from "@storybook/test";
const sampleData = [
{ value: "Ohio", label: "Ohio" },
{ value: "Pennsylvania", label: "Pennsylvania" },
{ value: "New York", label: "New York" },
{ value: "Maryland", label: "Maryland" }
{ value: "ohio", label: "Ohio" },
{ value: "pennsylvania", label: "Pennsylvania" },
{ value: "new_york", label: "New York" },
{ value: "maryland", label: "Maryland" }
];
</script>

<Template let:args>
<BasicDropdown {...args} on:change />
<BasicDropdown
variant="primary"
data={sampleData}
inlineLabel="Dropdown label"
{...args}
on:change
/>
</Template>

<Story
name="Default"
args={{
id: "dropdown-story",
dropdownWidth: 260,
inlineLabel: "Dropdown label",
arrowFillColor: "#1696D1",
placeholder: "Select a state",
data: sampleData
inlineLabel: "Dropdown with selected value"
}}
play={async ({ canvasElement }) => {
const canvas = within(canvasElement);
const selectEl = canvas.getByLabelText("Dropdown with selected value", {
selector: "select"
});
await fireEvent.change(selectEl, { target: { value: sampleData[1].value } });
expect(selectEl.value).toBe(sampleData[1].value);
await fireEvent.change(selectEl, { target: { value: sampleData[3].value } });
expect(selectEl.value).toBe(sampleData[3].value);
}}
/>

<Story
name="With value specified"
args={{
id: "dropdown-story-2",
inlineLabel: "Dropdown with value",
placeholder: "Select a state",
value: "Ohio",
data: sampleData
value: "pennsylvania"
}}
/>

<Story
name="With value selected"
name="With placeholder set to null and no value set (auto-selects first)"
args={{
id: "dropdown-story-3",
inlineLabel: "Dropdown with selected value",
placeholder: "Select a state",
data: sampleData
id: "dropdown-story-4",
placeholder: null
}}
play={async ({ canvasElement }) => {
const canvas = within(canvasElement);
const selectEl = canvas.getByLabelText("Dropdown with selected value", {
selector: "select"
});
await fireEvent.change(selectEl, { target: { value: sampleData[1].value } });
expect(selectEl.value).toBe(sampleData[1].value);
await fireEvent.change(selectEl, { target: { value: sampleData[3].value } });
expect(selectEl.value).toBe(sampleData[3].value);
/>

<Story
name="Secondary variant (blue) with label shown"
args={{
variant: "secondary-blue",
id: "dropdown-story-5",
inlineLabel: "Select a state",
showLabel: true,
placeholder: null
}}
/>

<Story
name="With placeholder set to null and no value set"
name="Secondary variant (black) with label shown"
args={{
id: "dropdown-story-4",
inlineLabel: "Dropdown without a value",
placeholder: null,
data: sampleData
variant: "secondary-black",
id: "dropdown-story-6",
inlineLabel: "Select a state",
showLabel: true,
placeholder: null
}}
/>

<Story
name="Secondary variant (yellow) with label shown"
args={{
variant: "secondary-yellow",
id: "dropdown-story-7",
inlineLabel: "Select a state",
showLabel: true,
placeholder: null
}}
/>

<Story
name="Secondary variant (blue) with label hidden"
args={{
variant: "secondary-blue",
id: "dropdown-story-8",
placeholder: "Select a state"
}}
/>

<Story
name="Secondary variant (black) with label hidden"
args={{
variant: "secondary-black",
id: "dropdown-story-9",
placeholder: "Select a state"
}}
/>

<Story
name="Secondary variant (yellow) with label hidden"
args={{
variant: "secondary-yellow",
id: "dropdown-story-10",
placeholder: "Select a state"
}}
/>

<Story name="Custom icon (uncommon)">
<BasicDropdown
variant="primary"
id="dropdown-story-11"
data={sampleData}
inlineLabel="Dropdown label"
on:change
>
<IconDownload slot="icon" size={16} fill={urbanColors.blue_shade_darker} />
</BasicDropdown>
</Story>
159 changes: 117 additions & 42 deletions src/lib/BasicDropdown/BasicDropdown.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
<script>
import { urbanColors } from "$lib/utils";
import IconChevronFull from "./IconChevronFull.svelte";
import IconChevronOutline from "./IconChevronOutline.svelte";
import urbanColors from "$lib/utils/urbanColors.js";
// define arguments
/**
* Variant of dropdown
* @type {string} ["primary" | "secondary-blue" | "secondary-black" | "secondary-yellow"]
*/
export let variant = "primary";
/**
* Unique id given to the dropdown DOM node
Expand All @@ -22,11 +28,17 @@
export let data;
/**
* Label for the dropdown (currently hidden)
* Label for the dropdown (used for accessibility even if showLabel is set to false)
* @type {string}
*/
export let inlineLabel;
/**
* Show label above dropdown
* @type {boolean}
*/
export let showLabel = false;
/**
* placeholder for when no option is selected (accepts a null value)
* @type {string | null} [placeholder="Select..."]
Expand All @@ -38,63 +50,126 @@
* @type {number}
*/
export let dropdownWidth = 260;
/**
* Hex color for arrow fill
* @type {string}
*/
export let arrowFillColor = urbanColors.blue;
// define the svg arrow
let arrow = `data:image/svg+xml;utf8,<svg width='16' height='10' viewBox='0 0 16 10' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M15.1313 0.666626C15.5179 0.666626 15.7794 0.846014 15.9272 1.20479C16.0749 1.56356 15.9954 1.85507 15.7111 2.09052L8.65117 9.12027C8.45791 9.26602 8.2419 9.33329 8.00316 9.33329C7.76442 9.33329 7.57115 9.26602 7.42335 9.12027L0.283802 2.09052C-0.000415318 1.85507 -0.0686276 1.55235 0.0677969 1.20479C0.21559 0.846014 0.477071 0.666626 0.863607 0.666626H15.1313Z' fill='${encodeURIComponent(
arrowFillColor
)}'/></svg>`;
</script>

<div class="dropdown-container">
<label aria-hidden="true" hidden for={id}>{inlineLabel}</label>
<select
bind:value
name={id}
{id}
class="dropdown-select"
style={`--bg-img: url("${arrow}"); width: ${dropdownWidth}px;`}
aria-label={inlineLabel}
on:change
>
<!-- options -->
{#if placeholder}
<option value={null}>{placeholder}</option>
{/if}
{#each data as d (d.value)}
{#if d.value !== ""}
<option value={d.value}>{d.label}</option>
<div class="dropdown-parent">
<label aria-hidden="true" hidden={!showLabel} for={id}>{inlineLabel} </label>
<div class="dropdown-container" style:width={`${dropdownWidth}px `}>
<select
bind:value
name={id}
{id}
class={`dropdown-select ${variant}`}
aria-label={inlineLabel}
on:change
>
<!-- options -->
{#if placeholder}
<option value={null}>{placeholder}</option>
{/if}
{/each}
</select>
{#each data as d (d.value)}
{#if d.value !== ""}
<option value={d.value}>{d.label}</option>
{/if}
{/each}
</select>
<div class="icons">
<span class="dropdown-chevron">
<slot name="icon">
{#if variant === "primary"}
<IconChevronFull />
{:else if variant === "secondary-blue" || variant === "secondary-black"}
<IconChevronOutline />
{:else if variant === "secondary-yellow"}
<IconChevronOutline fill={urbanColors.black} />
{/if}
</slot>
</span>
</div>
</div>
</div>

<style>
.dropdown-container {
.dropdown-parent {
display: flex;
align-items: center;
flex-direction: column;
gap: var(--spacing-2);
}
.dropdown-select {
.dropdown-container {
position: relative;
}
label {
font-size: var(--font-size-small);
text-transform: uppercase;
color: var(--color-gray-shade-darker);
padding: var(--spacing-2) var(--spacing-8) var(--spacing-2) var(--spacing-3);
}
select {
cursor: pointer;
text-overflow: ellipsis;
font-size: var(--font-size-normal);
font-family: Lato, helvetica, sans-serif;
border: 1px solid var(--color-gray-shade-medium);
cursor: pointer;
background-color: var(--color-white);
background-image: var(--bg-img);
background-size: var(--spacing-4) var(--spacing-4);
background-repeat: no-repeat;
background-position: 95% center;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
text-overflow: ellipsis;
width: 100%;
}
.dropdown-select.primary {
color: var(--color-gray-shade-darker);
padding: var(--spacing-2) var(--spacing-8) var(--spacing-2) var(--spacing-3);
border: 1px solid var(--color-gray-shade-medium);
background-color: var(--color-white);
}
.dropdown-select[class*="secondary-"] {
padding: var(--spacing-1) var(--spacing-4);
font-size: var(--font-size-normal);
font-weight: var(--font-weight-bold);
border-width: 1px;
border-style: solid;
line-height: 150%;
}
.dropdown-select.secondary-blue {
color: var(--color-white);
background-color: var(--color-blue);
border-color: var(--color-blue);
}
.dropdown-select.secondary-black {
color: var(--color-white);
background-color: var(--color-black);
border-color: var(--color-black);
}
.dropdown-select.secondary-yellow {
color: var(--color-black);
background-color: var(--color-yellow);
border-color: var(--color-yellow);
}
.icons {
pointer-events: none;
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 0;
display: flex;
flex-direction: row-reverse;
align-items: center;
padding: 0 var(--spacing-3);
}
.dropdown-chevron {
width: var(--spacing-4);
height: var(--spacing-4);
}
</style>
Loading

0 comments on commit 21a1227

Please sign in to comment.