Skip to content

Commit

Permalink
feat: input component
Browse files Browse the repository at this point in the history
Creates a new Input component with the same basic support as an
HTML input, but with our styling. Supports 4 types so far:
- normal text
- search (adds a search icon on the left)
- clear (adds an X on the right, which clears the field unless
  the parent event handler overrides it)
- password (with a show/hide button on the right ( unless the
  parent event handler disables it)

I don't want to give any impression that this component is
fully done or supports all cases, my intention is to get a good
base component that does enough to support the initial use, then
expand it as necessary to support different uses or additional
style requirements.

In order to give something to test, the first few uses of Input
are included:
- Main nav page search bar
- Settings > Preferences search bar
- Settings > Preferences string and file items

First step of #3234.

Signed-off-by: Tim deBoer <git@tdeboer.ca>
  • Loading branch information
deboer-tim committed Feb 8, 2024
1 parent 5c5d589 commit cf49b4f
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 67 deletions.
35 changes: 10 additions & 25 deletions packages/renderer/src/lib/preferences/PreferencesRendering.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { ContextUI } from '../context/context';
import { context } from '/@/stores/context';
import { onDestroy, onMount } from 'svelte';
import { type Unsubscriber } from 'svelte/store';
import Input from '../ui/Input.svelte';
export let properties: IConfigurationPropertyRecordedSchema[] = [];
export let key: string;
Expand Down Expand Up @@ -66,31 +67,15 @@ function updateSearchValue(event: any) {

<Route path="/" breadcrumb="{key}">
<SettingsPage title="Preferences">
<div class="mt-4" slot="header">
<div
class="flex items-center text-gray-700 rounded-sm rounded-lg border-2 border-charcoal-900 focus-within:border-violet-500">
<input
on:input="{e => updateSearchValue(e)}"
class="w-full bg-charcoal-900 py-1 px-3 outline-0 text-sm"
name="search"
type="text"
placeholder="Search preferences"
id="input-search"
aria-label="input-search" />
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-4 h-4 mr-2 bg-charcoal-900"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
</div>
<Input
slot="header"
on:input="{e => updateSearchValue(e)}"
class="mt-4"
name="search"
type="search"
placeholder="Search preferences"
id="input-search"
aria-label="input-search" />
<div class="flex flex-col space-y-5">
{#if matchingRecords.size === 0}
<div>No Settings Found</div>
Expand Down
25 changes: 7 additions & 18 deletions packages/renderer/src/lib/preferences/item-formats/FileItem.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<script lang="ts">
import type { IConfigurationPropertyRecordedSchema } from '../../../../../main/src/plugin/configuration-registry';
import Fa from 'svelte-fa';
import { faXmark } from '@fortawesome/free-solid-svg-icons';
import Button from '../../ui/Button.svelte';
import Input from '../../ui/Input.svelte';
export let record: IConfigurationPropertyRecordedSchema;
export let value: string;
export let onChange = async (_id: string, _value: string) => {};
Expand All @@ -17,34 +16,24 @@ async function selectFilePath() {
}
}
function handleCleanValue(
event: MouseEvent & {
currentTarget: EventTarget & HTMLButtonElement;
},
) {
function handleCleanValue(event: CustomEvent<MouseEvent>) {
if (record.id) onChange(record.id, '');
event.preventDefault();
}
</script>

<div class="w-full flex">
<input
class="grow {!value ? 'mr-3' : ''} py-1 px-2 outline-0 text-sm placeholder-gray-900 bg-zinc-700"
<Input
class="grow mr-2"
name="{record.id}"
readonly
type="text"
type="clear"
placeholder="{record.placeholder}"
value="{value || ''}"
id="input-standard-{record.id}"
aria-invalid="{invalidEntry}"
aria-label="{record.description}" />
<button
class="relative cursor-pointer right-5"
class:hidden="{!value}"
aria-label="clear"
on:click="{event => handleCleanValue(event)}">
<Fa icon="{faXmark}" />
</button>
aria-label="{record.description}"
on:action="{event => handleCleanValue(event)}" />
<Button
on:click="{() => selectFilePath()}"
id="rendering.FilePath.{record.id}"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<script lang="ts">
import type { IConfigurationPropertyRecordedSchema } from '../../../../../main/src/plugin/configuration-registry';
import Input from '../../ui/Input.svelte';
export let record: IConfigurationPropertyRecordedSchema;
export let value: string | undefined;
export let value: string;
export let onChange = async (_id: string, _value: string) => {};
let invalidEntry = false;
Expand All @@ -13,11 +14,10 @@ function onInput(event: Event) {
}
</script>

<input
<Input
on:input="{onInput}"
class="grow py-1 px-2 w-full outline-0 border-b-2 border-gray-800 hover:border-violet-500 focus:border-violet-500 placeholder-gray-900 bg-zinc-700"
class="grow"
name="{record.id}"
type="text"
placeholder="{record.placeholder}"
bind:value="{value}"
readonly="{!!record.readonly}"
Expand Down
115 changes: 115 additions & 0 deletions packages/renderer/src/lib/ui/Input.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**********************************************************************
* Copyright (C) 2024 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/

/* eslint-disable @typescript-eslint/no-explicit-any */

import '@testing-library/jest-dom/vitest';
import { test, expect } from 'vitest';
import { render, screen } from '@testing-library/svelte';
import Input from './Input.svelte';

function renderInput(
type: 'text' | 'search' | 'password' | 'clear',
value: string,
placeholder: string,
readonly: boolean,
onClick?: any,
): void {
render(Input, { type: type, value: value, placeholder: placeholder, readonly: readonly, onClick: onClick });
}

test('Expect basic styling', async () => {
const value = 'test';
renderInput('text', value, value, false);

const element = screen.getByPlaceholderText(value);
expect(element).toBeInTheDocument();
expect(element).toHaveClass('px-1');
expect(element).toHaveClass('outline-0');
expect(element).toHaveClass('bg-charcoal-500');
expect(element).toHaveClass('text-sm');
expect(element).toHaveClass('text-white');

expect(element).toHaveClass('group-hover:bg-charcoal-900');
expect(element).toHaveClass('group-focus-within:bg-charcoal-900');
expect(element).toHaveClass('group-hover-placeholder:text-gray-900');

expect(element.parentElement).toBeInTheDocument();
expect(element.parentElement).toHaveClass('bg-charcoal-500');
expect(element.parentElement).toHaveClass('border-[1px]');
expect(element.parentElement).toHaveClass('border-charcoal-500');

expect(element.parentElement).toHaveClass('hover:bg-charcoal-900');
expect(element.parentElement).toHaveClass('hover:rounded-md');
expect(element.parentElement).toHaveClass('hover:border-purple-400');
});

test('Expect basic readonly styling', async () => {
const value = 'test';
renderInput('text', value, value, true);

const element = screen.getByPlaceholderText(value);
expect(element).toBeInTheDocument();
expect(element).toHaveClass('px-1');
expect(element).toHaveClass('outline-0');
expect(element).toHaveClass('bg-charcoal-500');
expect(element).toHaveClass('text-sm');
expect(element).toHaveClass('text-white');

expect(element).not.toHaveClass('group-hover:bg-charcoal-900');
expect(element).not.toHaveClass('group-focus-within:bg-charcoal-900');
expect(element).not.toHaveClass('group-hover-placeholder:text-gray-900');

expect(element.parentElement).toBeInTheDocument();
expect(element.parentElement).toHaveClass('bg-charcoal-500');
expect(element.parentElement).toHaveClass('border-[1px]');
expect(element.parentElement).toHaveClass('border-charcoal-500');
expect(element.parentElement).toHaveClass('border-b-charcoal-100');

expect(element.parentElement).not.toHaveClass('hover:bg-charcoal-900');
expect(element.parentElement).not.toHaveClass('hover:rounded-md');
expect(element.parentElement).not.toHaveClass('hover:border-purple-400');
});

test('Expect search type', async () => {
const value = 'test';
renderInput('search', value, value, false);

const element = screen.getByRole('img');
expect(element).toBeInTheDocument();
});

test('Expect clear type', async () => {
const value = 'test';
renderInput('clear', value, value, false);

const element = screen.getByRole('button');
expect(element).toBeInTheDocument();
expect(element).toHaveAttribute('aria-label', 'clear');
expect(element).toHaveClass('cursor-pointer');
});

test('Expect password type', async () => {
const value = 'test';
renderInput('password', value, value, false);

const element = screen.getByRole('button');
expect(element).toBeInTheDocument();
expect(element).toHaveAttribute('aria-label', 'show/hide');
expect(element).toHaveClass('cursor-pointer');
});
111 changes: 111 additions & 0 deletions packages/renderer/src/lib/ui/Input.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<script lang="ts">
import { faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
import { faXmark } from '@fortawesome/free-solid-svg-icons';
import { createEventDispatcher, onMount } from 'svelte';
import Fa from 'svelte-fa';
export let placeholder: string | undefined = undefined;
export let id: string | undefined = undefined;
export let name: string | undefined = undefined;
export let value: string | undefined = undefined;
export let readonly: boolean = false;
export let required: boolean = false;
export let type: 'text' | 'search' | 'clear' | 'password' = 'text';
export let passwordHidden: boolean = true;
let element: HTMLInputElement;
const dispatch = createEventDispatcher();
onMount(() => {
if (type === 'password') {
element.type = 'password';
}
});
// clear the value if the parent doesn't override
async function onClear() {
if (dispatch('action', { cancelable: true })) {
value = '';
}
}
// show/hide if the parent doesn't override
async function onShowHide() {
if (dispatch('action', { cancelable: true })) {
passwordHidden = !passwordHidden;
element.type = passwordHidden ? 'password' : 'text';
}
}
</script>

<div
class="flex flex-row items-center px-1 py-1 group bg-charcoal-500 border-[1px] border-charcoal-500 {$$props.class ||
''}"
class:hover:bg-charcoal-900="{!readonly}"
class:focus-within:bg-charcoal-900="{!readonly}"
class:hover:rounded-md="{!readonly}"
class:focus-within:rounded-md="{!readonly}"
class:border-b-purple-500="{!readonly}"
class:hover:border-purple-400="{!readonly}"
class:focus-within:border-purple-500="{!readonly}"
class:border-b-charcoal-100="{readonly}">
{#if type === 'search'}
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-4 h-4 mx-1 text-gray-700"
class:group-hover:text-gray-900="{!readonly}"
class:group-focus-within:text-gray-900="{!readonly}"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
role="img">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
{/if}
<input
bind:this="{element}"
on:input
class="w-full px-1 outline-0 bg-charcoal-500 text-sm text-white placeholder:text-gray-700"
class:group-hover:bg-charcoal-900="{!readonly}"
class:group-focus-within:bg-charcoal-900="{!readonly}"
class:group-hover-placeholder:text-gray-900="{!readonly}"
name="{name}"
type="text"
readonly="{readonly}"
required="{required}"
placeholder="{placeholder}"
id="{id}"
aria-label="{$$props['aria-label']}"
aria-invalid="{$$props['aria-invalid']}"
bind:value="{value}" />
{#if type === 'password'}
<button
class="px-1 cursor-pointer text-gray-700"
class:group-hover:text-gray-900="{!readonly}"
class:group-focus-within:text-gray-900="{!readonly}"
class:hidden="{!value}"
aria-label="show/hide"
on:click="{onShowHide}"
>{#if passwordHidden}
<Fa icon="{faEye}" />
{:else}
<Fa icon="{faEyeSlash}" />
{/if}
</button>
{:else if type === 'clear'}
<button
class="px-1 cursor-pointer text-gray-700"
class:group-hover:text-gray-900="{!readonly}"
class:group-focus-within:text-gray-900="{!readonly}"
class:hidden="{!value || readonly}"
aria-label="clear"
on:click="{onClear}">
<Fa icon="{faXmark}" />
</button>
{/if}
</div>
23 changes: 3 additions & 20 deletions packages/renderer/src/lib/ui/NavPage.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script lang="ts">
import Input from './Input.svelte';
export let title: string;
export let searchTerm = '';
export let searchEnabled = true;
Expand All @@ -21,26 +23,7 @@ export let searchEnabled = true;
{#if searchEnabled}
<div class="flex flex-row pb-4" role="region" aria-label="search">
<div class="pl-5 lg:w-[35rem] w-[22rem]">
<div class="flex items-center bg-charcoal-800 text-gray-700 rounded-sm">
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-5 h-5 ml-2 mr-2"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
<input
bind:value="{searchTerm}"
type="text"
name="containerSearchName"
placeholder="Search {title}...."
class="w-full py-2 outline-none text-sm bg-charcoal-800 rounded-sm text-gray-700 placeholder-gray-700" />
</div>
<Input bind:value="{searchTerm}" name="containerSearchName" type="search" placeholder="Search {title}...." />
</div>
<div class="flex flex-1 px-5" role="group" aria-label="bottomAdditionalActions">
{#if $$slots['bottom-additional-actions']}
Expand Down

0 comments on commit cf49b4f

Please sign in to comment.