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

feat(OnyxSelectInput): support validation #1408

Merged
merged 32 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
4104049
implement validity for onyxselect
BoppLi Jun 20, 2024
4d2062e
rename selection to modelValue so validityChange events fire
BoppLi Jun 21, 2024
ddb6384
refactor OnyxSelectInput API
BoppLi Jun 21, 2024
da1729d
Merge remote-tracking branch 'origin/main' into feat/1302-support-val…
BoppLi Jun 21, 2024
a660014
include select in form demo + improve layout
BoppLi Jun 21, 2024
584664b
docs(changeset): feat(global.css): include layers order to prevent hi…
BoppLi Jun 21, 2024
d939b59
fix visibility error for error tooltip
BoppLi Jun 21, 2024
b31a9ad
fix select input story/test
BoppLi Jun 21, 2024
1331cd8
use onyxSelect for language selection
BoppLi Jun 21, 2024
53c9e66
implement composable to synchronize selectOptions
BoppLi Jun 21, 2024
e2703c6
update composable
BoppLi Jun 24, 2024
56f158d
remove redundant composable
BoppLi Jun 24, 2024
3fa7ad0
cleanup
BoppLi Jun 24, 2024
01f75d2
Merge remote-tracking branch 'origin/main' into feat/1302-support-val…
BoppLi Jun 24, 2024
ca90103
cleanup readonly logic for OnyxSelectInput. instead, prevent typing.
BoppLi Jun 24, 2024
c0b0b69
fix styles
BoppLi Jun 25, 2024
24d6803
fix focus during select search
BoppLi Jun 25, 2024
d0f0c61
fix border color for readonly
BoppLi Jun 25, 2024
33330ee
rename screenshot label
BoppLi Jun 25, 2024
948ca0a
allow all navigational keys for OnyxSelectInput
BoppLi Jun 25, 2024
bd3f841
chore: update Playwright screenshots (#1407)
github-actions[bot] Jun 25, 2024
c014cd6
add docs
BoppLi Jun 26, 2024
db89e10
Merge remote-tracking branch 'origin/main' into feat/1302-support-val…
BoppLi Jun 26, 2024
2103610
fix combobox
BoppLi Jun 26, 2024
2072401
add component test
BoppLi Jun 26, 2024
6cfa79b
docs(changeset): feat(OnyxSelectInput): support validity handling
BoppLi Jun 26, 2024
e7319d3
update changelog
BoppLi Jun 26, 2024
461cd4a
docs(changeset): feat(createComboBox): open flyout on text input for …
BoppLi Jun 26, 2024
787ba4f
add screenshot tests for invalid
BoppLi Jun 26, 2024
4aeeac1
chore: update Playwright screenshots (#1416)
github-actions[bot] Jun 26, 2024
2425dee
Merge branch 'main' into feat/1302-support-validation-for-select
BoppLi Jun 26, 2024
b00586c
add docs for layers
BoppLi Jun 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cold-doors-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sit-onyx/headless": minor
---

feat(createComboBox): open flyout on text input for searchable combo boxes. Fix close/open bug when selecting with space
8 changes: 8 additions & 0 deletions .changeset/rare-cheetahs-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"sit-onyx": minor
---

feat(OnyxSelectInput): support validity handling

- internal input is not readonly, but blocks all character inputs
- supports translated error message for empty required inputs
5 changes: 5 additions & 0 deletions .changeset/unlucky-otters-marry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"sit-onyx": minor
---

feat(global.css): include layers order to prevent hierarchy issues
29 changes: 26 additions & 3 deletions apps/alpha-test-app/src/components/LanguageSelection.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,34 @@
<script lang="ts" setup>
import { OnyxRadioGroup, type SelectOption } from "sit-onyx";
import { OnyxSelect, type SelectOption } from "sit-onyx";
import { computed } from "vue";
import { useI18n } from "vue-i18n";

const locale = defineModel<string | undefined>();

const { t } = useI18n();
const supportedLocales = ["en-US", "de-DE", "ko-KR"];
const options: SelectOption[] = supportedLocales.map((value) => ({ value, label: value }));
const options: SelectOption<string>[] = supportedLocales.map((value) => ({ value, label: value }));

const selectModel = computed({
get: () => options.find((option) => option.value === locale.value),
set: (newLocale) => (locale.value = newLocale?.value),
});
</script>

<template>
<OnyxRadioGroup v-model="locale" headline="Select language" :options="options" />
<OnyxSelect
v-model="selectModel"
class="language"
label="Select language"
list-label="List label"
:options="options"
:message="`Preview: ${t('message')} in ${locale}`"
/>
</template>

<style lang="scss" scoped>
.language {
margin-bottom: var(--onyx-spacing-sm);
max-width: 15rem;
}
</style>
105 changes: 78 additions & 27 deletions apps/alpha-test-app/src/components/form-demo/FormDemo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import {
OnyxHeadline,
OnyxInput,
OnyxRadioGroup,
OnyxSelect,
OnyxSwitch,
OnyxTextarea,
type CheckboxGroupOption,
type OnyxRadioGroupProps,
type RadioButtonOption,
type SelectOption,
} from "sit-onyx";
import { ref } from "vue";

Expand All @@ -23,11 +25,27 @@ export type FormData = Partial<{
patternInput: string;
switch: boolean;
checkboxGroup: number[];
requiredSelect: SelectOption[];
radioGroup: OnyxRadioGroupProps["modelValue"];
}>;

const formState = defineModel<FormData>();

const selectOptions = [
{
value: "apple",
label: "Apple",
},
{
value: "banana",
label: "Banana",
},
{
value: "strawberry",
label: "Strawberry",
},
];

const customErrorExample = ref("");

const onPatternValidityChange = (state: ValidityState) => {
Expand All @@ -50,50 +68,92 @@ const radioOptions: RadioButtonOption[] = [
</script>

<template>
<form v-if="formState" class="demo" @submit.prevent="handleSubmit" @reset="formState = {}">
<OnyxHeadline is="h2" class="demo__headline"
<form
v-if="formState"
class="demo onyx-grid"
@submit.prevent="handleSubmit"
@reset="formState = {}"
>
<OnyxHeadline is="h3" class="onyx-grid-span-20"
>This form is currently <span class="demo__invalid">in</span>valid.</OnyxHeadline
>

<OnyxInput v-model="formState.defaultInput" label="Default" />
<OnyxInput v-model="formState.defaultInput" class="onyx-grid-span-4" label="Default" />

<OnyxInput v-model="formState.requiredInput" label="Requires a value" required />
<OnyxInput
v-model="formState.requiredInput"
class="onyx-grid-span-4"
label="Requires a value"
required
/>

<OnyxInput
v-model="formState.minlengthInput"
class="onyx-grid-span-4"
label="Minlength 5"
type="text"
:minlength="5"
required
/>
<OnyxInput
v-model="formState.typeInput"
class="onyx-grid-span-4"
label="Type email"
type="email"
/>

<OnyxInput
v-model="formState.patternInput"
class="onyx-grid-span-4"
label="Pattern lowercase characters"
pattern="[a-z ]*"
:custom-error="customErrorExample"
@validity-change="onPatternValidityChange"
/>

<OnyxSelect
v-model="formState.requiredSelect"
class="onyx-grid-span-4"
label="Example select"
list-label="List label"
multiple
:options="selectOptions"
placeholder="Placeholder..."
required
/>

<OnyxTextarea v-model="formState.requiredTextarea" label="Requires a value" required />
<OnyxTextarea
v-model="formState.requiredTextarea"
class="onyx-grid-span-4"
label="Requires a value"
required
/>

<OnyxTextarea
v-model="formState.minlengthTextarea"
class="onyx-grid-span-4"
label="Minlength 5"
type="text"
:minlength="5"
required
/>

<OnyxInput v-model="formState.typeInput" label="Type email" type="email" />
<OnyxSwitch v-model="formState.switch" class="onyx-grid-span-4" label="Switch" required />

<OnyxInput
v-model="formState.patternInput"
label="Pattern lowercase characters"
pattern="[a-z ]*"
:custom-error="customErrorExample"
@validity-change="onPatternValidityChange"
<OnyxCheckboxGroup
v-model="formState.checkboxGroup"
class="onyx-grid-span-4"
:options="checkboxOptions"
/>

<OnyxSwitch v-model="formState.switch" label="Switch" required />

<OnyxCheckboxGroup v-model="formState.checkboxGroup" :options="checkboxOptions" />

<OnyxRadioGroup v-model="formState.radioGroup" :options="radioOptions" required />
<OnyxRadioGroup
v-model="formState.radioGroup"
class="onyx-grid-span-4"
:options="radioOptions"
required
/>

<div class="demo__actions">
<div class="demo__actions onyx-grid-span-20">
<OnyxButton label="Reset" color="neutral" type="reset" />
<OnyxButton class="demo__submit" label="Submit" type="submit" />
</div>
Expand All @@ -102,10 +162,6 @@ const radioOptions: RadioButtonOption[] = [

<style lang="scss" scoped>
.demo {
display: flex;
flex-direction: column;
gap: var(--onyx-spacing-md);

&:valid {
.demo__invalid {
display: none;
Expand All @@ -116,10 +172,5 @@ const radioOptions: RadioButtonOption[] = [
display: flex;
gap: var(--onyx-spacing-xs);
}

.onyx-input,
.onyx-textarea {
max-width: 20rem;
}
}
</style>
4 changes: 2 additions & 2 deletions apps/alpha-test-app/src/stores/grid-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { defineStore } from "pinia";
import { ref } from "vue";

export const useGridStore = defineStore("grid", () => {
const isMaxWidth = ref(false);
const isCentered = ref(false);
const isMaxWidth = ref(true);
const isCentered = ref(true);

return { isMaxWidth, isCentered };
});
22 changes: 10 additions & 12 deletions apps/alpha-test-app/src/views/FormDemoView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useI18n } from "vue-i18n";
import LanguageSelection from "../components/LanguageSelection.vue";
import FormDemo, { type FormData } from "../components/form-demo/FormDemo.vue";

const { t, locale } = useI18n();
const { locale } = useI18n();

const validFormData = ref<FormData>({
defaultInput: "No Validation",
Expand All @@ -27,31 +27,29 @@ const invalidFormData = ref<FormData>({

<template>
<OnyxPageLayout>
<template #sidebar>
<div class="sidebar">
<LanguageSelection v-model="locale" />
<div class="onyx-grid-container">
<section class="header">
<OnyxHeadline is="h1" element="h1">Form Demos</OnyxHeadline>

<p>"{{ t("message") }}" in {{ locale }}</p>
</div>
</template>
<LanguageSelection v-model="locale" />
</section>

<div class="page">
<OnyxHeadline is="h1" element="h1">Initially Invalid example</OnyxHeadline>
<OnyxHeadline is="h2" element="h1">Initially Invalid example</OnyxHeadline>
<FormDemo v-model="invalidFormData" />
<pre class="state">State: {{ invalidFormData }}</pre>

<hr />

<OnyxHeadline is="h1" element="h1">Initially Valid example</OnyxHeadline>
<OnyxHeadline is="h2" element="h1">Initially Valid example</OnyxHeadline>
<FormDemo v-model="validFormData" />
<pre class="state">State: {{ validFormData }}</pre>
</div>
</OnyxPageLayout>
</template>

<style lang="scss" scoped>
.page {
padding: var(--onyx-spacing-xl);
.header {
margin-bottom: var(--onyx-spacing-lg);
}
.sidebar {
padding: var(--onyx-spacing-3xs);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const expectToClose = async (
* Test an implementation of the combobox based on https://w3c.github.io/aria/#combobox
*/
export const comboboxTesting = async (
page: Page,
_page: Page,
listbox: Locator,
combobox: Locator,
button: Locator,
Expand Down
20 changes: 12 additions & 8 deletions packages/headless/src/composables/comboBox/createComboBox.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { computed, ref, unref, type MaybeRef, type Ref } from "vue";
import { computed, unref, type MaybeRef, type Ref } from "vue";
import { createBuilder } from "../../utils/builder";
import { createId } from "../../utils/id";
import { isPrintableCharacter, wasKeyPressed, type PressedKey } from "../../utils/keyboard";
Expand All @@ -12,8 +12,13 @@ import { useTypeAhead } from "../typeAhead";

export type ComboboxAutoComplete = "none" | "list" | "both";

const OPENING_KEYS: PressedKey[] = ["ArrowDown", "ArrowUp", " ", "Enter", "Home", "End"];
const CLOSING_KEYS: PressedKey[] = ["Escape", { key: "ArrowUp", altKey: true }, "Enter", "Tab"];
export const OPENING_KEYS: PressedKey[] = ["ArrowDown", "ArrowUp", " ", "Enter", "Home", "End"];
export const CLOSING_KEYS: PressedKey[] = [
"Escape",
{ key: "ArrowUp", altKey: true },
"Enter",
"Tab",
];
const SELECTING_KEYS_SINGLE: PressedKey[] = ["Enter", " "];
const SELECTING_KEYS_MULTIPLE: PressedKey[] = ["Enter"];

Expand Down Expand Up @@ -117,7 +122,6 @@ export const createComboBox = createBuilder(
onActivatePrevious,
templateRef,
}: CreateComboboxOptions<TValue, TAutoComplete, TMultiple>) => {
const inputValid = ref(true);
const controlsId = createId("comboBox-control");

const autocomplete = computed(() => unref(autocompleteRef));
Expand All @@ -126,10 +130,6 @@ export const createComboBox = createBuilder(

const handleInput = (event: Event) => {
const inputElement = event.target as HTMLInputElement;
inputValid.value = inputElement.validity.valid;
if (!unref(isExpanded)) {
onToggle?.();
}

if (autocomplete.value !== "none") {
onAutocomplete?.(inputElement.value);
Expand Down Expand Up @@ -193,6 +193,10 @@ export const createComboBox = createBuilder(
!isExpanded.value && onToggle?.();
return typeAhead(event);
}
if (autocomplete.value !== "none" && isPrintableCharacter(event.key)) {
!isExpanded.value && onToggle?.();
return;
}
return handleNavigation(event);
};

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ const props = withDefaults(defineProps<OnyxButtonProps>(), {
const { densityClass } = useDensity(props);

const emit = defineEmits<{
/** Emitted when the button is clicked (and is not disabled). */
/**
* Emitted when the button is clicked (and is not disabled).
*/
click: [];
}>();
</script>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ test.describe("Screenshot tests", () => {
disabledAccessibilityRules: ["color-contrast"],
component: (column, row) => {
const label =
column === "long-text" ? "Very long label that should be truncated" : "Test label";
column === "long-text" ? "Very very long label that should be truncated" : "Test label";
const message =
column === "long-text" ? "Very long message that should be truncated" : "Test message";
const labelTooltip = "More information";
Expand Down Expand Up @@ -103,12 +103,6 @@ test.describe("Screenshot tests", () => {
},
});

/*
message
invalid
error tooltip
counter
*/
executeMatrixScreenshotTest({
name: "Input (message replacement on invalid)",
columns: ["default", "long-text", "with-counter"],
Expand Down
Loading