Skip to content

Commit

Permalink
Get rid of <sl-icon>
Browse files Browse the repository at this point in the history
## Description

The SVG project icons can't be included via `<img>` because they
are colored using `currentColor`. They also can't be included via
Parcel's `bundle-text` named pipeline becase

The way we used `<sl-icon>`, it was fetching the svg file over the
network and dynamically inserting it into the DOM. This instead uses
another Parcel plugin to import an svg file as a React component at
build time. Getting it to work is kinda finicky. Ssee
parcel-bundler/parcel#7587, plus the workaround I ended up using in
the not-merged PR parcel-bundler/parcel#7711. The list of transformers
for `jsx:*.{js,jsx}` is copied from Parcel's default config for those
file types.

The icons are set to width `1em` to allow them to be sized by the
font-size at the usage site, like `<sl-icon>`.

## Test Plan

Look at the project icons in all contexts where they appear: project
multiselect, icon tab bar in pill mode, icon tab bar in dropdown mode.
  • Loading branch information
oyamauchi committed Jan 29, 2024
1 parent a75ec48 commit f087001
Show file tree
Hide file tree
Showing 8 changed files with 448 additions and 53 deletions.
14 changes: 14 additions & 0 deletions .parcelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"extends": "@parcel/config-default",
"transformers": {
"jsx:*.svg": [
"...",
"@parcel/transformer-svg-react"
],
"jsx:*.{js,jsx}": [
"@parcel/transformer-babel",
"@parcel/transformer-js",
"@parcel/transformer-react-refresh-wrap"
]
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"@headlessui/tailwindcss": "^0.2.0",
"@lit/localize-tools": "^0.7.1",
"@parcel/transformer-inline-string": "2.10.2",
"@parcel/transformer-svg-react": "2.10.2",
"@swc/helpers": "^0.4.14",
"@types/react": "^18.2.46",
"@types/react-dom": "^18.2.18",
Expand Down
30 changes: 11 additions & 19 deletions src/components/select.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { flip, useFloating } from '@floating-ui/react-dom';
import { Listbox, Transition } from '@headlessui/react';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
import SlSpinner from '@shoelace-style/shoelace/dist/react/spinner';
import clsx from 'clsx';
import { Fragment } from 'react';
Expand All @@ -10,7 +9,7 @@ import { FormLabel } from './form-label';
export type Option<T extends string> = {
value: T;
label: string;
iconURL?: URL;
getIcon?: () => React.ReactElement;
};

export type SelectProps<T extends string> = {
Expand Down Expand Up @@ -49,7 +48,7 @@ export type SelectProps<T extends string> = {
);

/** Renders the tags in the multiselect. */
const renderTag = (label: string, iconURL?: URL) => (
const renderTag = (label: string, icon?: () => React.ReactElement) => (
<div
className={clsx(
'flex',
Expand All @@ -63,12 +62,10 @@ const renderTag = (label: string, iconURL?: URL) => (
'text-sm',
// If this has an icon, it may have long text; let the browser
// shrink it as necessary
iconURL && 'min-w-0',
icon && 'min-w-0',
)}
>
{iconURL && (
<SlIcon className="text-base text-grey-700" src={iconURL.toString()} />
)}
{icon && <span className="text-base text-grey-700">{icon()}</span>}
<span className="whitespace-nowrap overflow-hidden text-ellipsis">
{label}
</span>
Expand Down Expand Up @@ -100,7 +97,7 @@ export const Select = <T extends string>({
if (currentValue.length > 0) {
buttonContents = (
<div className="grow ml-1 py-1 h-full min-w-0 flex gap-1">
{renderTag(firstCurrentOption!.label, firstCurrentOption!.iconURL)}
{renderTag(firstCurrentOption!.label, firstCurrentOption!.getIcon)}
{currentValue.length > 1 && renderTag(`+${currentValue.length - 1}`)}
</div>
);
Expand All @@ -109,12 +106,10 @@ export const Select = <T extends string>({
if (firstCurrentOption) {
buttonContents = (
<div className="grow ml-3 flex gap-2 items-center">
{firstCurrentOption.iconURL && (
<SlIcon
className="text-lg text-grey-700"
src={firstCurrentOption.iconURL.toString()}
aria-hidden={true}
/>
{firstCurrentOption.getIcon && (
<span className="text-lg text-grey-700" aria-hidden={true}>
{firstCurrentOption.getIcon()}
</span>
)}
<div className={clsx(disabled && 'text-grey-500')}>
{firstCurrentOption.label}
Expand Down Expand Up @@ -235,11 +230,8 @@ export const Select = <T extends string>({
) : (
<div className="w-5" />
))}
{o.iconURL && (
<SlIcon
className="text-lg text-grey-700"
src={o.iconURL.toString()}
/>
{o.getIcon && (
<span className="text-lg text-grey-700">{o.getIcon()}</span>
)}
{o.label}
</Listbox.Option>
Expand Down
10 changes: 4 additions & 6 deletions src/icon-tab-bar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { msg } from '@lit/localize';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
import clsx from 'clsx';
import { FC } from 'react';
import { Option, Select } from './components/select';
Expand Down Expand Up @@ -45,10 +44,9 @@ export const IconTabBar: FC<Props> = ({ tabs, selectedTab, onTabSelected }) => {
aria-label={PROJECTS[project].label()}
onClick={() => onTabSelected(project)}
>
<SlIcon
className="text-lg" // 20px
src={PROJECTS[project].iconURL.toString()}
></SlIcon>
<span className="text-lg">
{PROJECTS[project].getIcon() /* 20px */}
</span>
<div className="text-base leading-tight font-medium whitespace-nowrap">
{shortLabel(project)}
</div>
Expand All @@ -59,7 +57,7 @@ export const IconTabBar: FC<Props> = ({ tabs, selectedTab, onTabSelected }) => {
const options: Option<Project>[] = tabs.map(project => ({
value: project,
label: PROJECTS[project].label(),
iconURL: PROJECTS[project].iconURL,
getIcon: PROJECTS[project].getIcon,
}));

return (
Expand Down
17 changes: 6 additions & 11 deletions src/parcel.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,11 @@ declare module 'bundle-text:*' {
}

/**
* We use the magic "locales" scheme to import files that are generated by
* lit-localize. Running lit-localize and resolving these magic specifiers
* happen in scripts/parcel-resolver-locale.mjs.
* Import an SVG file as "jsx:./path/to/icon.svg" to import it as if it were a
* JSX <svg> element. This is done with a Parcel transformer.
*/
declare module 'locales:config' {
export const sourceLocale: string;
export const targetLocales: string[];
export const allLocales: string[];
}
declare module 'locales:*' {
import { TemplateMap } from '@lit/localize';
export const templates: TemplateMap;
declare module 'jsx:*.svg' {
import { FunctionComponent, SVGProps } from 'react';
const svg: FunctionComponent<SVGProps<SVGSVGElement>>;
export default svg;
}
30 changes: 19 additions & 11 deletions src/projects.ts → src/projects.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { msg } from '@lit/localize';
import BatteryIcon from 'jsx:../static/icons/battery.svg';
import ClothesDryerIcon from 'jsx:../static/icons/clothes-dryer.svg';
import CookingIcon from 'jsx:../static/icons/cooking.svg';
import ElectricalWiringIcon from 'jsx:../static/icons/electrical-wiring.svg';
import EvIcon from 'jsx:../static/icons/ev.svg';
import HvacIcon from 'jsx:../static/icons/hvac.svg';
import SolarIcon from 'jsx:../static/icons/solar.svg';
import WaterHeaterIcon from 'jsx:../static/icons/water-heater.svg';
import WeatherizationIcon from 'jsx:../static/icons/weatherization.svg';
import { ItemType } from './api/calculator-types-v1';

type ProjectInfo = {
label: () => string;
shortLabel?: () => string;
// The first argument for the URL must be a string literal for the correct Parcel behavior: https://parceljs.org/languages/javascript/#url-dependencies
iconURL: URL;
getIcon: () => React.ReactElement;
items: ItemType[];
};

Expand Down Expand Up @@ -33,7 +41,7 @@ export const PROJECTS: Record<Project, ProjectInfo> = {
heat_pump_clothes_dryer: {
items: ['heat_pump_clothes_dryer'],
label: () => msg('Clothes dryer'),
iconURL: new URL('/static/icons/clothes-dryer.svg', import.meta.url),
getIcon: () => <ClothesDryerIcon width="1em" />,
},
hvac: {
items: [
Expand All @@ -45,7 +53,7 @@ export const PROJECTS: Record<Project, ProjectInfo> = {
msg('HVAC', {
desc: 'short label for "heating, ventilation & cooling"',
}),
iconURL: new URL('/static/icons/hvac.svg', import.meta.url),
getIcon: () => <HvacIcon width="1em" />,
},
ev: {
items: [
Expand All @@ -55,41 +63,41 @@ export const PROJECTS: Record<Project, ProjectInfo> = {
],
label: () => msg('Electric vehicle'),
shortLabel: () => msg('EV', { desc: 'short label for "electric vehicle"' }),
iconURL: new URL('/static/icons/ev.svg', import.meta.url),
getIcon: () => <EvIcon width="1em" />,
},
solar: {
items: ['rooftop_solar_installation'],
label: () => msg('Solar', { desc: 'i.e. rooftop solar' }),
iconURL: new URL('/static/icons/solar.svg', import.meta.url),
getIcon: () => <SolarIcon width="1em" />,
},
battery: {
items: ['battery_storage_installation'],
label: () => msg('Battery storage'),
iconURL: new URL('/static/icons/battery.svg', import.meta.url),
getIcon: () => <BatteryIcon width="1em" />,
},
heat_pump_water_heater: {
items: ['heat_pump_water_heater'],
label: () => msg('Water heater'),
iconURL: new URL('/static/icons/water-heater.svg', import.meta.url),
getIcon: () => <WaterHeaterIcon width="1em" />,
},
cooking: {
items: ['electric_stove'],
label: () => msg('Cooking stove/range'),
shortLabel: () =>
msg('Cooking', { desc: 'short label for stove/range incentives' }),
iconURL: new URL('/static/icons/cooking.svg', import.meta.url),
getIcon: () => <CookingIcon width="1em" />,
},
wiring: {
items: ['electric_panel', 'electric_wiring'],
label: () => msg('Electrical panel & wiring'),
shortLabel: () =>
msg('Electrical', { desc: 'short for "electrical panel and wiring"' }),
iconURL: new URL('/static/icons/electrical-wiring.svg', import.meta.url),
getIcon: () => <ElectricalWiringIcon width="1em" />,
},
weatherization_and_efficiency: {
items: ['weatherization', 'efficiency_rebates'],
label: () => msg('Weatherization & efficiency'),
shortLabel: () => msg('Weatherization'),
iconURL: new URL('/static/icons/weatherization.svg', import.meta.url),
getIcon: () => <WeatherizationIcon width="1em" />,
},
};
2 changes: 1 addition & 1 deletion src/state-calculator-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ const renderProjectsField = (
.map(([value, data]) => ({
value: value as Project,
label: data.label(),
iconURL: data.iconURL,
getIcon: data.getIcon,
}))
.sort((a, b) => a.label.localeCompare(b.label))}
currentValue={projects}
Expand Down
Loading

0 comments on commit f087001

Please sign in to comment.