Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add searchable select #339

Merged
merged 21 commits into from Aug 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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