Skip to content

Commit

Permalink
Add searchable select (#339)
Browse files Browse the repository at this point in the history
  • Loading branch information
DTCurrie committed Aug 29, 2023
1 parent 27b2073 commit fd61b89
Show file tree
Hide file tree
Showing 30 changed files with 1,508 additions and 139 deletions.
5 changes: 4 additions & 1 deletion packages/core/package.json
Expand Up @@ -37,7 +37,9 @@
},
"dependencies": {
"@mdi/js": "^7.2.96",
"classnames": "^2.3.2"
"classnames": "^2.3.2",
"lodash": "^4.17.21",
"nanoid": "^4.0.2"
},
"devDependencies": {
"@floating-ui/dom": "^1.5.1",
Expand All @@ -49,6 +51,7 @@
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/svelte": "^4.0.3",
"@testing-library/user-event": "^14.4.3",
"@types/lodash": "^4.14.197",
"@types/testing-library__jest-dom": "^5.14.9",
"@typescript-eslint/eslint-plugin": "^6.4.0",
"@typescript-eslint/parser": "^6.4.0",
Expand Down
87 changes: 87 additions & 0 deletions packages/core/plugins.ts
@@ -0,0 +1,87 @@
import plugin from 'tailwindcss/plugin';
import type { OptionalConfig } from 'tailwindcss/types/config';

export const plugins = [
plugin(({ addUtilities, matchUtilities, theme }) => {
addUtilities({
'.scrollbar-thin': {
'scrollbar-width': 'thin',
},
});

matchUtilities(
{
scrollbar: (value: string) => ({
scrollbarColor: value,
}),
},
{ values: theme('colors') ?? {} }
);

matchUtilities(
{
'scrollbar-w': (value: string) => ({
'&::-webkit-scrollbar': {
width: value,
},
}),
},
{ values: theme('spacing') ?? {} }
);

matchUtilities(
{
'scrollbar-track': (value: string) => ({
'&::-webkit-scrollbar-track': {
background: value,
},
}),
},
{ values: theme('colors') ?? {} }
);

matchUtilities(
{
'scrollbar-thumb': (value: string) => ({
'&::-webkit-scrollbar-thumb': {
backgroundColor: value,
},
}),
},
{ values: theme('colors') ?? {} }
);

matchUtilities(
{
'scrollbar-thumb-border': (value: string) => ({
'&::-webkit-scrollbar-thumb': {
borderRadius: value,
},
}),
},
{ values: theme('borderRadius') ?? {} }
);

matchUtilities(
{
'scrollbar-thumb-border': (value: string) => ({
'&::-webkit-scrollbar-thumb': {
borderWidth: value,
},
}),
},
{ values: theme('borderWidth') ?? {} }
);

matchUtilities(
{
'scrollbar-thumb-border': (value: string) => ({
'&::-webkit-scrollbar-thumb': {
borderColor: value,
},
}),
},
{ values: theme('borderColor') ?? {} }
);
}),
] satisfies OptionalConfig['plugins'];
28 changes: 28 additions & 0 deletions packages/core/src/lib/__tests__/unique-id.spec.ts
@@ -0,0 +1,28 @@
import { describe, expect, it } from 'vitest';
import { useUniqueId } from '$lib';

describe('useUniqueId', () => {
it('should return a unique ID with the default prefix', () => {
const first = useUniqueId();
const second = useUniqueId();
const third = useUniqueId();

expect(first.includes('uid_')).toBeTruthy();
expect(second.includes('uid_')).toBeTruthy();
expect(third.includes('uid_')).toBeTruthy();

expect(new Set([first, second, third]).size).toBe(3);
});

it('should return a unique ID with the passed prefix', () => {
const first = useUniqueId('test');
const second = useUniqueId('test');
const third = useUniqueId('test');

expect(first.includes('test_')).toBeTruthy();
expect(second.includes('test_')).toBeTruthy();
expect(third.includes('test_')).toBeTruthy();

expect(new Set([first, second, third]).size).toBe(3);
});
});
2 changes: 1 addition & 1 deletion packages/core/src/lib/button/__tests__/icon-button.spec.ts
Expand Up @@ -5,7 +5,7 @@ import { IconButton } from '$lib';
describe('IconButton', () => {
const common = { icon: 'close', label: 'close' };
it('Renders a button in the style of the primary variant if no variant is specified', () => {
render(IconButton);
render(IconButton, common);
expect(screen.getByRole('button')).toHaveClass(
'text-gray-6',
'hover:border-medium'
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/lib/button/button.svelte
Expand Up @@ -44,7 +44,7 @@ export let width: 'full' | 'default' = 'default';
let extraClasses: cx.Argument = '';
export { extraClasses as cx };
const dispatch = createEventDispatcher<{ click: undefined }>();
const dispatch = createEventDispatcher<{ click: null }>();
const onClick = () => {
if (disabled) {
Expand Down
48 changes: 28 additions & 20 deletions packages/core/src/lib/icon/icon.svelte
Expand Up @@ -11,23 +11,11 @@ A component that renders SVG icons from the @mdi/js package
-->
<svelte:options immutable />

<script lang="ts">
import cx from 'classnames';
import { paths } from './icons';
<script
lang="ts"
context="module"
>
type Size = 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl';
/**
* The size of the icon.
*/
export let size: Size = 'base';
/**
* The name of the icon.
*/
export let name = '';
const hasNameProperty = Object.hasOwn(paths, name);
const sizes: Record<Size, string> = {
xs: 'w-3 h-3',
sm: 'w-3.5 h-3.5',
Expand All @@ -40,15 +28,35 @@ const sizes: Record<Size, string> = {
};
</script>

<script lang="ts">
import cx from 'classnames';
import { paths } from './icons';
/** The name of the icon. */
export let name: string;
/** The size of the icon. */
export let size: Size = 'base';
/** Additional CSS classes to pass to the button. */
let extraClasses: cx.Argument = '';
export { extraClasses as cx };
const hasNameProperty = Object.hasOwn(paths, name);
</script>

<!--
Accessibility approach for icon svgs taken from:
https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-label
-->
<svg
class={cx(sizes[size], {
'inline-block': hasNameProperty,
hidden: !hasNameProperty,
})}
class={cx(
sizes[size],
{
hidden: !hasNameProperty,
},
extraClasses
)}
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/lib/index.ts
Expand Up @@ -11,6 +11,7 @@ export { default as Pill } from './pill.svelte';
export { default as Switch } from './switch.svelte';
export { default as Radio } from './radio.svelte';
export { default as Tabs } from './tabs.svelte';
export { useUniqueId } from './unique-id';

export {
default as Tooltip,
Expand All @@ -34,10 +35,13 @@ export {
type TextInputTypes,
} from './input/text-input.svelte';

export { default as Select, type SelectState } from './select/select.svelte';
export { default as SearchableSelect } from './select/searchable-select.svelte';
export { type SortOptions } from './select/search';

export { default as Table, type TableVariant } from './table/table.svelte';
export { default as TableHeader } from './table/table-header.svelte';
export { default as TableHeaderCell } from './table/table-header-cell.svelte';
export { default as TableBody } from './table/table-body.svelte';
export { default as TableRow } from './table/table-row.svelte';
export { default as TableCell } from './table/table-cell.svelte';
export { default as Select } from './select/select.svelte';
12 changes: 6 additions & 6 deletions packages/core/src/lib/input/__tests__/input.spec.ts
Expand Up @@ -13,7 +13,7 @@ describe('Input', () => {
);

expect(input).not.toHaveClass(
'bg-disabled-light text-disabled-dark border-disabled-light pointer-events-none'
'bg-disabled-light text-disabled-dark border-disabled-light cursor-not-allowed'
);
});

Expand All @@ -27,7 +27,7 @@ describe('Input', () => {
);

expect(input).toHaveClass(
'bg-disabled-light text-disabled-dark border-disabled-light pointer-events-none'
'bg-disabled-light text-disabled-dark border-disabled-light cursor-not-allowed'
);

expect(input).toHaveAttribute('aria-disabled', 'true');
Expand All @@ -42,7 +42,7 @@ describe('Input', () => {
'h-[30px] w-full appearance-none border px-2 py-1.5 text-xs leading-tight outline-none'
);

expect(input).toHaveClass('bg-light border-none');
expect(input).toHaveClass('bg-light border-transparent');
expect(input).not.toHaveAttribute('aria-disabled');
});

Expand All @@ -54,7 +54,7 @@ describe('Input', () => {
const svg = container.querySelector('svg');

expect(svg).toBeInTheDocument();
expect(svg?.parentElement).toHaveClass('text-info-dark');
expect(svg).toHaveClass('text-info-dark');
});

it('Renders the input in the warn state', () => {
Expand All @@ -65,7 +65,7 @@ describe('Input', () => {
const svg = container.querySelector('svg');

expect(svg).toBeInTheDocument();
expect(svg?.parentElement).toHaveClass('text-warning-bright');
expect(svg).toHaveClass('text-warning-bright');
});

it('Renders the input in the error state', () => {
Expand All @@ -76,6 +76,6 @@ describe('Input', () => {
const svg = container.querySelector('svg');

expect(svg).toBeInTheDocument();
expect(svg?.parentElement).toHaveClass('text-danger-dark');
expect(svg).toHaveClass('text-danger-dark');
});
});
31 changes: 15 additions & 16 deletions packages/core/src/lib/input/input.svelte
Expand Up @@ -37,7 +37,6 @@ export let state: InputState | undefined = 'none';
/** The HTML input element. */
export let input: HTMLInputElement | undefined = undefined;
// Assert this element will be defined by the time it is used by the parent.
$: isInfo = state === 'info';
$: isWarn = state === 'warn';
Expand All @@ -61,14 +60,12 @@ $: icon = {
class={cx(
'h-[30px] w-full appearance-none border px-2 py-1.5 text-xs leading-tight outline-none',
{
'border-light bg-white hover:border-gray-6 focus:border-gray-9':
'border-light hover:border-gray-6 focus:border-gray-9':
!disabled && !readonly && !isError,
'border-none bg-light': readonly,
'pointer-events-none border-disabled-light bg-disabled-light text-disabled-dark':
'bg-light focus:border-gray-9 border-transparent': readonly,
'border-disabled-light focus:border-disabled-dark bg-disabled-light text-disabled-dark cursor-not-allowed select-none':
disabled,
'border-light hover:border-medium focus:border-gray-9 ':
!disabled && !isError,
'border-danger-dark focus:outline-[1.5px] focus:-outline-offset-1 focus:outline-danger-dark':
'border-danger-dark focus:outline-danger-dark focus:outline-[1.5px] focus:-outline-offset-1':
isError,
}
)}
Expand All @@ -80,15 +77,17 @@ $: icon = {
/>

{#if icon !== ''}
<span
class={cx('absolute right-2', {
'text-info-dark': isInfo,
'text-warning-bright': isWarn,
'text-danger-dark': isError,
})}
>
<Icon name={icon} />
</span>
<Icon
cx={[
'absolute right-2 top-1.5',
{
'text-info-dark': isInfo,
'text-warning-bright': isWarn,
'text-danger-dark': isError,
},
]}
name={icon}
/>
{/if}
</div>

Expand Down
8 changes: 1 addition & 7 deletions packages/core/src/lib/input/numeric-input.svelte
Expand Up @@ -16,13 +16,7 @@ import type { NumericInputTypes } from './utils';
/** The input type */
export let type: NumericInputTypes | undefined = 'number';
/**
* The value of the input, if any.
*
* TODO: Discuss disabling these rules for svelte components, otherwise
* these props are treatef as required and force users to add value={undefined}
* when no initial value is set.
*/
/** The value of the input, if any. */
export let value: number | undefined = undefined;
/** The amount to increment/decrement when using the up/down arrows. */
Expand Down

0 comments on commit fd61b89

Please sign in to comment.