Skip to content

Commit

Permalink
Merge pull request #8 from canvas/jsb/headless-ui
Browse files Browse the repository at this point in the history
headlessui react select box
  • Loading branch information
JeremyBernier authored Dec 15, 2023
2 parents faaf940 + 9ad11e8 commit 3b9d32f
Show file tree
Hide file tree
Showing 9 changed files with 134 additions and 56 deletions.
4 changes: 4 additions & 0 deletions example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ This sample application demonstrates how to use Canvas embeds in your applicatio
npm i
npm run dev
```

## Local development with React repo

The `canvas-embed` repo is hosted in the `react` folder. You can link to the local version via: `npm link ../react` for local development
8 changes: 4 additions & 4 deletions example/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
}
28 changes: 4 additions & 24 deletions react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,10 @@ npm install canvas-embed
## Usage

```
import { Chart } from "canvas-embed";
import { Canvas } from "canvas-embed";
<Chart
chartId={embedId} // From a chart you built in Canvas
authToken={token} // Generated from your backend application using one of our clients
disableExport={true} // Set if you want a dropdown allowing the chart to be downloaded
timezone={null} // Set if you want date times to be adjusted to a timezone
<Canvas
canvasId={canvasId}
authToken={AUTH_TOKEN}
/>
```

## Internal

### Development

Copy over Rust types from main repo with (adjust relative path in script as necessary):

```
npm run copy-rust-types
```

### Publishing

```
npm version patch
npm run build
npm run publish --access public
```
30 changes: 30 additions & 0 deletions react/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@
"react-dom": "^18.2.0"
},
"dependencies": {
"@headlessui/react": "^1.7.17",
"@heroicons/react": "^2.0.18",
"antd": "^5.12.2",
"chroma-js": "^2.4.2",
"highcharts": "^11.2.0",
Expand Down
2 changes: 1 addition & 1 deletion react/src/Element.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function Element({ title, children, elementId }: ElementProps): React.Rea
}}
id={elementId}
>
<div className="flex h-12 cursor-grab items-center rounded-lg px-7 hover:bg-highlight/50">
<div className="flex h-12 items-center rounded-lg px-7 hover:bg-highlight/50">
<div className="flex flex-1 items-center">
<div style={styles.title}>{title}</div>
</div>
Expand Down
99 changes: 80 additions & 19 deletions react/src/components/MultiSelectInput.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,89 @@
import React from 'react';
import React, { useState, Fragment } from 'react';
import { Listbox, Transition } from '@headlessui/react';
import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid';

type Value = string;
type Label = string;
export type SelectOption = [Value, Label];

type MultiSelectInputProps = {
value: string;
value: string; // value === '' when default option is selected
onChange: (value: string) => void;
options: string[][];
options: SelectOption[];
defaultOption?: string;
};

function getLabel(item: SelectOption) {
if (item[0] === '') return item[1];
return `${item[0]}${item[1] ? ` (${item[1]})` : ''}`;
}

// taken from https://headlessui.com/react/listbox
const MultiSelectInputDisplay = ({ value, onChange, options }: MultiSelectInputProps) => {
return (
<div className="w-72 z-30">
<Listbox value={value} onChange={(item) => onChange(item[0])}>
<div className="relative mt-1">
<Listbox.Button className="relative w-full cursor-pointer rounded-lg bg-white py-2 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white/75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 sm:text-sm">
<span className="block truncate">
{getLabel(options.find((item) => item[0] === value) as SelectOption)}
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</span>
</Listbox.Button>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm">
{options?.map((option, index) => (
<Listbox.Option
key={index}
className={({ active }) =>
`relative cursor-pointer select-none py-2 pl-10 pr-4 ${
active ? 'bg-amber-100 text-amber-900' : 'text-gray-900'
}`
}
value={option}
>
{({ selected }) => (
<>
<span
className={`block truncate ${selected ? 'font-medium' : 'font-normal'}`}
>
{getLabel(option)}
</span>
{selected ? (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-amber-600">
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</Listbox>
</div>
);
};

const MultiSelectInput = ({ value, onChange, options }: MultiSelectInputProps) => {
// Our wrapper that adds default option
const MultiSelectInput = ({ value, onChange, options, defaultOption }: MultiSelectInputProps) => {
const optionsFinal = defaultOption ? [['', defaultOption] as SelectOption, ...options] : options;
const valueFinal = value || '';
return (
<select
id="countries"
className="cursor-pointer border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-300 focus:border-blue-300 block w-full px-2 py-1 w-32"
onChange={(event) => {
onChange(event.target.value);
}}
value={value || ''}
>
<option selected value="">
Select Filter
</option>
{options.map((option) => (
<option value={option[0]}>{`${option[0]} (${option[1]})`}</option>
))}
</select>
<MultiSelectInputDisplay
value={valueFinal}
onChange={onChange}
options={optionsFinal}
defaultOption={defaultOption}
/>
);
};

Expand Down
15 changes: 8 additions & 7 deletions react/src/filter/Filters.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,36 @@
import React from 'react';
import isEmpty from 'lodash/isEmpty';
import MultiSelectInput from '../components/MultiSelectInput';
import useCanvasState from '../state/useCanvasState';
import isEmpty from 'lodash/isEmpty';

import { GetCanvasEmbedResponse } from '@/src/rust_types/GetCanvasEmbedResponse';

export function Filters({ canvasData }: { canvasData: GetCanvasEmbedResponse }) {
const filters = canvasData?.filters?.filters;
const filtersVisible = filters?.filter((filter) => filter?.filterType?.type === 'select');
const updateFilter = useCanvasState((state) => state.updateFilter);
const selectedFilters = useCanvasState((state) => state.filters);
const valueSelected = !isEmpty(selectedFilters);
const valueIsSelected = !isEmpty(selectedFilters);

return (
<section>
{filtersVisible?.map((filter) => (
<div className="flex gap-3">
<MultiSelectInput
key={filter.filterId}
value={selectedFilters[filter.variable]}
onChange={(value: string) => {
if (value === '') {
onChange={(item: string) => {
if (item === '' || item == null) {
updateFilter({});
return;
}
const variable = filter.variable;
updateFilter({ [variable]: value });
updateFilter({ [variable]: item });
}}
defaultOption="Select Filter"
// @ts-ignore
options={canvasData.filters.uniqueValues[filter.filterType.storeId]}
/>
{valueSelected && (
{valueIsSelected && (
<button onClick={() => updateFilter({})} className="text-xs text-blue-700">
Clear Filter
</button>
Expand Down

0 comments on commit 3b9d32f

Please sign in to comment.