Broke out a Select component

This commit is contained in:
palewire 2023-04-03 08:08:14 -04:00
parent 4b8be6a562
commit a1085893b8
10 changed files with 155 additions and 75 deletions

View file

@ -1,5 +1,13 @@
import type { ComponentType } from 'svelte';
/**
* Used for the list of <option> tags nested in a <select> input.
*/
export type Option = {
value: string;
text: string;
};
/**
* Used for any props that restrict width of a container to one of pre-fab widths.
*/

View file

@ -0,0 +1,78 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import type { Option } from '../@types/global';
/**
* The label that appears above the select input.
* @type {string}
*/
export let label: string = '';
/**
* The label that appears above the select input.
* @type {Array}
*/
export let options: Option[] = [];
const dispatch = createEventDispatcher();
function input(event) {
const value = event.target.value;
dispatch('select', { value });
}
</script>
<div class="select">
{#if label}
<label for="select--input">{label}</label>
{/if}
<select
class="select--input"
name="select--input"
id="select--input"
on:input="{input}"
>
{#each options as obj}
<option value="{obj.value}">{obj.text}</option>
{/each}
</select>
</div>
<style lang="scss">
@import '../../scss/colours/thematic/tr';
@import '../../scss/fonts/variables';
.select {
width: 256px;
font-family: $font-family-display;
label {
display: block;
font-size: 0.8rem;
font-weight: 300;
color: $tr-dark-grey;
padding: 0 0 0.125rem 0;
}
.select--input {
position: relative;
font-size: 0.8rem;
font-weight: 500;
height: 33px;
border: 1px solid $tr-muted-grey;
border-radius: 6px;
width: 100%;
padding: 0.5rem;
-moz-appearance: none; /* Firefox */
-webkit-appearance: none; /* Safari and Chrome */
appearance: none; /* Remove the default arrow */
padding-right: 20px; /* Add some padding to make room for a custom arrow */
background: transparent;
background-image: url('data:image/svg+xml;utf8,<svg width="15" height="9" viewBox="0 0 15 9" xmlns="http://www.w3.org/2000/svg"><path d="M6.76474 8.30466L0.236082 1.54523C-0.0786943 1.21934 -0.0786943 0.69069 0.236082 0.364804C0.550521 0.0392666 1.19794 0.0403099 1.51305 0.364804L7.33483 6.49522L12.9249 0.475171C13.3549 0.0451683 14.1195 0.0396141 14.4339 0.365152C14.7487 0.691037 14.7487 1.21969 14.4339 1.54557L7.90492 8.30466C7.59015 8.63054 7.07952 8.63054 6.76474 8.30466Z" fill="gray"/></svg>');
background-repeat: no-repeat;
background-position-x: 235px;
background-position-y: 55%;
}
.select--input::-ms-expand {
display: none; /* Remove the default arrow in Internet Explorer 11 */
}
}
</style>

View file

@ -129,7 +129,8 @@
filterField: 'Region',
paginated: true,
title: 'Press Freedom Index',
notes:
dek: 'Reporters Without Borders ranks countries based on their level of press freedom using criteria such as the degree of media pluralism and violence against journalists.',
source:
"Source: <a href='https://en.wikipedia.org/wiki/Press_Freedom_Index'>Reporters Without Borders</a>",
}}"
/>
@ -155,11 +156,11 @@
args="{{
data: richestWomen,
title: 'The Richest Women in the World',
notes:
source:
"Source: <a href='https://www.forbes.com/sites/rachelsandler/2022/04/05/the-top-richest-women-in-the-world-2022/?sh=29c2f69446a8'>Forbes</a>",
sortable: true,
sortField: 'Net worth (in billions)',
sortDirection: 'descending',
fieldFormatters: { 'Net worth (in billions)': (v) => '$' + v },
fieldFormatters: { 'Net worth (in billions)': (v) => '$' + v.toFixed(1) },
}}"
/>

View file

@ -128,11 +128,12 @@
import Block from '../Block/Block.svelte';
import Pagination from './Pagination.svelte';
import Search from './Search.svelte';
import Select from './Select.svelte';
import SortArrow from './SortArrow.svelte';
import {
filterArray,
paginateArray,
uniqueAttr,
getOptions,
isNumeric,
} from './utils.js';
@ -140,9 +141,7 @@
let showAll = false;
let pageNumber = 1;
let searchText = '';
const filterList = filterField
? uniqueAttr(data, filterField).sort((a, b) => a.localeCompare(b))
: undefined;
const filterList = filterField ? getOptions(data, filterField) : undefined;
let filterValue = '';
$: filteredData = filterArray(data, searchText, filterField, filterValue);
$: sortedData = sortArray(filteredData, sortField, sortDirection);
@ -174,8 +173,8 @@
}
function handleFilterInput(event) {
const value = event.target.value;
filterValue = value === 'all' ? '' : value;
const value = event.detail.value;
filterValue = value === 'All' ? '' : value;
pageNumber = 1;
}
@ -239,31 +238,21 @@
{/if}
{#if searchable || filterList}
<section class="input">
{#if searchable}
<Search
bind:searchPlaceholder
on:search="{handleSearchInput}"
/>
{/if}
{#if filterList}
{#if searchable}<div
style="clear: both; display: block; padding-top: 1rem;"
></div>{/if}
<div class="filter">
<label for="filter--select"
>{#if filterLabel}{filterLabel}{:else}Filter by {filterField}{/if}</label
>
<select
class="filter--select"
name="filter--select"
id="filter--select"
on:input="{handleFilterInput}"
>
<option value="all">All</option>
{#each filterList as object}
<option value="{object}">{object}</option>
{/each}
</select>
<div class="table--caption--filter">
<Select
label="{filterLabel || filterField}"
options="{filterList}"
on:select="{handleFilterInput}"
/>
</div>
{/if}
{#if searchable}
<div class="table--caption--search">
<Search
bind:searchPlaceholder
on:search="{handleSearchInput}"
/>
</div>
{/if}
</section>
@ -455,25 +444,13 @@
section.input {
margin: 0.5rem 0 0 0;
padding: 0;
font-size: 1rem;
width: 100%;
label {
line-height: 1.333;
display: block;
font-size: 1.125rem;
font-family: $font-family-display;
font-weight: 500;
}
.filter {
.filter--select {
padding: 0.33rem;
font-size: 1.1rem;
height: 2.3rem;
border: 1px solid $tr-muted-grey;
border-radius: 5px;
width: 300px;
}
}
display: flex;
justify-content: flex-start;
align-items: flex-end;
flex-direction: row;
flex-wrap: wrap;
gap: 1rem;
}
nav.show-all {

View file

@ -2,10 +2,12 @@ Feel free to both search and filter.
```svelte
<Table
data={yourData},
searchable=true,
filterField='Region',
title='Press Freedom Index',
notes='Source: <a href="https://en.wikipedia.org/wiki/Press_Freedom_Index">Reporters Without Borders</a>',
data="{yourData}",
searchable="{true}",
filterField="{'Region'}",
paginated="{true}",
title="{'Press Freedom Index'}",
dek="{'Reporters Without Borders ranks countries based on their level of press freedom using criteria such as the degree of media pluralism and violence against journalists.'}",
source="{'Source: <a href="https://en.wikipedia.org/wiki/Press_Freedom_Index">Reporters Without Borders</a>'}",
/>
```

View file

@ -2,9 +2,10 @@ Allow users to filter the table by providing one of the attributes as the `filte
```svelte
<Table
data={yourData},
filterField='Region',
title='Press Freedom Index',
notes='Source: <a href="https://en.wikipedia.org/wiki/Press_Freedom_Index">Reporters Without Borders</a>',
data="{yourData}",
filterField="{'Region'}",
paginate="{true}"
title="{'Press Freedom Index'}",
notes="{'Source: <a href="https://en.wikipedia.org/wiki/Press_Freedom_Index">Reporters Without Borders</a>'}",
/>
```

View file

@ -1,19 +1,21 @@
Provide custom formatters by providing functions keyed to your field names to the `fieldFormatters` option. Columns are still sorted by the raw, underlying values. This can be used to provide the unit of measurement with numeric fields.
Format columns by supplying functions keyed to field names with the `fieldFormatters` option. Columns are still sorted using the raw, underlying values.
Among other things, this feature can be used to provide a unit of measurement with numeric fields.
```svelte
<script lang="ts">
const fieldFormatters = {
'Net worth (in billions)': (v) => '$' + v
'Net worth (in billions)': (v) => '$' + v.toFixed(1)
};
</script>
<Table
data={yourData}
fieldFormatters={fieldFormatters},
sortable=true,
sortField='Score',
sortDirection='descending',
title='Press Freedom Index',
notes='Source: <a href="https://en.wikipedia.org/wiki/Press_Freedom_Index">Reporters Without Borders</a>',
data="{yourData}"
fieldFormatters="{fieldFormatters}",
sortable="{true}",
sortField="{'Score'}",
sortDirection="{'descending'}",
title:"{'The Richest Women in the World'}",
source: "{'Source: Forbes'}",
/>
```

View file

@ -9,6 +9,6 @@ This is a good option when publishing large tables for readers to explore. It wo
data="{yourData}"
paginated="{true}"
title="{'Press Freedom Index'}"
source="{'<a href='https://en.wikipedia.org/wiki/Press_Freedom_Index'>Reporters Without Borders</a>'}"
source="{'<a href="https://en.wikipedia.org/wiki/Press_Freedom_Index">Reporters Without Borders</a>'}"
/>
```

View file

@ -2,10 +2,10 @@ Allow users to search the table by setting the optional `searchable` variable. M
```svelte
<Table
data="{yourData},"
searchable="{true},"
paginated="{true},"
title="{'Press Freedom Index'},"
notes="{'Source: <a href="https://en.wikipedia.org/wiki/Press_Freedom_Index">Reporters Without Borders</a>'},"
data="{yourData}",
searchable="{true}",
paginated="{true}",
title="{'Press Freedom Index'}",
notes="{'Source: <a href="https://en.wikipedia.org/wiki/Press_Freedom_Index">Reporters Without Borders</a>'}",
/>
```

View file

@ -24,6 +24,17 @@ export function unique(value, index, array) {
return array.indexOf(value) === index;
}
export function getOptions(data, attr) {
// Get all the unique values in the provided field. Sort it.
const attrList = uniqueAttr(data, attr).sort((a, b) => a.localeCompare(b));
// Tack 'All' as the front as the first option.
attrList.unshift('All');
// Convert the list into Option typed objects ready for our Select component
return attrList.map((a) => ({ text: a, value: a }));
}
export function isNumeric(value) {
return (
typeof value === 'number' ||