Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions packages/design-system/docs/components/OcTextInput/OcTextInput.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ The default and most simple use case involves a `v-model` and a `label`.
<<< @/components/OcTextInput/default.vue
:::

### Inline Label

::: livecode

```vue
<oc-text-input label="Your name" :inline-label="true" />
```

:::

### Disabled

::: livecode
Expand Down Expand Up @@ -69,4 +79,12 @@ The following input types ares supported.
<<< @/components/OcTextInput/messages.vue
:::

### Customization

`OcTextInput` is highly customizable
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The customization section description is incomplete. Add a period at the end of the sentence and expand the description to explain what customization options are demonstrated in the example.

Suggested change
`OcTextInput` is highly customizable
`OcTextInput` is highly customizable. The example below demonstrates how you can customize the component by adding custom icons, adjusting input styles, and modifying placeholder text to better fit your application's needs.

Copilot uses AI. Check for mistakes.

::: livecode {path=/components/OcTextInput/customization.vue}
<<< @/components/OcTextInput/customization.vue
:::

::: component-api
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<template>
<div class="border-b mb-2">
<oc-text-input class="mb-2" label="Your name" :inline-label="true" :has-border="false" />
</div>
<div class="border-b">
<oc-text-input class="mb-2" label="Full address" :inline-label="true" :has-border="false" />
</div>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ exports[`OcModal > displays input 1`] = `
<div class="oc-modal-body px-4 pt-4">
<!--v-if-->
<!--v-if-->
<oc-text-input-stub label="Folder name" id="oc-textinput-1" type="text" modelvalue="New folder" clearbuttonenabled="false" clearbuttonaccessiblelabel="" disabled="false" errormessagedebouncedtime="500" fixmessageline="true" readonly="false" requiredmark="false" passwordpolicy="[object Object]" class="oc-modal-body-input -mb-5 pb-4"></oc-text-input-stub>
<oc-text-input-stub label="Folder name" id="oc-textinput-1" type="text" modelvalue="New folder" clearbuttonenabled="false" clearbuttonaccessiblelabel="" disabled="false" inlinelabel="false" errormessagedebouncedtime="500" fixmessageline="true" readonly="false" requiredmark="false" passwordpolicy="[object Object]" hasborder="true" class="oc-modal-body-input -mb-5 pb-4"></oc-text-input-stub>
</div>
<div class="oc-modal-body-actions flex justify-end p-4 text-right">
<div class="oc-modal-body-actions-grid grid grid-flow-col auto-cols-1fr">
Expand Down
37 changes: 33 additions & 4 deletions packages/design-system/src/components/OcSelect/OcSelect.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
<template>
<div>
<label v-if="!labelHidden" :aria-hidden="true" :for="id" class="inline-block mb-0.5">
<div :class="{ 'flex items-center': inlineLabel }">
<label
v-if="!labelHidden"
:aria-hidden="true"
:for="id"
class="inline-block"
:class="{ 'mr-2': inlineLabel, 'mb-0.5': !inlineLabel }"
>
{{ label }}
<span v-if="requiredMark" class="text-role-error" aria-hidden="true">*</span>
</label>
Expand All @@ -19,7 +25,8 @@
:multiple="multiple"
class="oc-select bg-transparent"
:class="{
'oc-select-position-fixed': positionFixed
'oc-select-position-fixed': positionFixed,
'oc-select-no-border': !hasBorder
}"
:dropdown-should-open="selectDropdownShouldOpen"
:map-keydown="selectMapKeydown"
Expand Down Expand Up @@ -134,6 +141,11 @@ export interface Props {
* @docs The label of the select input.
*/
label: string
/**
* @docs Determines if the label will be displayed next to the select input field.
* @default false
* */
inlineLabel?: boolean
/**
* @docs Determines if the label is visually hidden. Note that it will still be read by screen readers.
* @default false
Expand Down Expand Up @@ -201,13 +213,19 @@ export interface Props {
* @default false
*/
requiredMark?: boolean
/**
* @docs Determines if the select input field has a surrounding border.
* @default true
*/
hasBorder?: boolean
}

export interface Emits {
/**
* @docs Emitted when the user has typed.
*/
(e: 'search:input', search: string): void

/**
* @docs Emitted when the user has selected an option.
*/
Expand All @@ -219,6 +237,7 @@ export interface Slots {
* @docs Slot for when an option is selected.
*/
'selected-option'?: () => unknown

/**
* @docs This component inherits all slots from `vue-select`. See https://vue-select.org/api/slots for more information.
*/
Expand Down Expand Up @@ -252,6 +271,7 @@ const {
},
disabled = false,
label,
inlineLabel = false,
labelHidden = false,
contextualHelper,
optionLabel = 'label',
Expand All @@ -265,7 +285,8 @@ const {
multiple = false,
readOnly = false,
positionFixed = false,
requiredMark = false
requiredMark = false,
hasBorder = true
} = defineProps<Props>()

const emit = defineEmits<Emits>()
Expand Down Expand Up @@ -485,6 +506,14 @@ export default { components: { VueSelect } }
}

.oc-select {
&-no-border {
.vs__dropdown-toggle {
border: none !important;
outline: none !important;
background-color: transparent !important;
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing the outline with '!important' will eliminate focus indicators, which are critical for keyboard navigation and accessibility. Ensure alternative focus styling is provided when hasBorder is false.

Suggested change
background-color: transparent !important;
background-color: transparent !important;
// Provide alternative focus indicator for accessibility
&:focus {
box-shadow: 0 0 0 2px var(--oc-role-outline);
}

Copilot uses AI. Check for mistakes.
}
}

&-position-fixed {
.vs__dropdown-menu {
position: fixed;
Expand Down
28 changes: 24 additions & 4 deletions packages/design-system/src/components/OcTextInput/OcTextInput.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
<template>
<div :class="$attrs.class">
<div :class="[{ 'flex items-center': inlineLabel }, $attrs.class]">
<slot name="label">
<label class="inline-block mb-0.5" :for="id">
<label
class="inline-block"
:class="{ 'mr-2': inlineLabel, 'mb-0.5': !inlineLabel }"
:for="id"
>
{{ label }}
<span v-if="requiredMark" class="text-role-error" aria-hidden="true">*</span>
</label>
</slot>
<div class="relative">
<div class="relative" :class="{ 'grow-1': inlineLabel }">
<oc-icon v-if="readOnly" name="lock" size="small" class="mt-2 ml-2 absolute" />
<component
:is="inputComponent"
Expand All @@ -18,7 +22,8 @@
:class="{
'oc-text-input-danger border-role-error': !!showErrorMessage,
'pl-6': !!readOnly,
'pr-6': showClearButton
'pr-6': showClearButton,
'border-none outline-none bg-transparent': !hasBorder
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using '!important' implicitly through 'border-none' and 'outline-none' may override necessary focus states. Consider using a more specific selector or ensure focus styles are still accessible when hasBorder is false.

Suggested change
'border-none outline-none bg-transparent': !hasBorder
'border-none bg-transparent focus:ring-2 focus:ring-primary': !hasBorder

Copilot uses AI. Check for mistakes.
}"
:type="type"
:value="modelValue"
Expand Down Expand Up @@ -130,6 +135,11 @@ export interface Props {
* @docs The label of the input element.
*/
label: string
/**
* @docs Determines if the label will be displayed next to the input field.
* @default false
* */
inlineLabel?: boolean
/**
* @docs The error message to be displayed below the input.
*/
Expand Down Expand Up @@ -166,6 +176,11 @@ export interface Props {
* @docs The method to generate a password if the `type` is set to `password`.
*/
generatePasswordMethod?: (...args: unknown[]) => string
/**
* @docs Determines if the input field has a surrounding border.
* @default true
*/
hasBorder?: boolean
}

export interface Emits {
Expand Down Expand Up @@ -212,12 +227,14 @@ const {
defaultValue,
disabled = false,
label,
inlineLabel = false,
errorMessage,
errorMessageDebouncedTime = 500,
fixMessageLine = false,
descriptionMessage,
readOnly = false,
requiredMark = false,
hasBorder = true,
passwordPolicy = {},
generatePasswordMethod
} = defineProps<Props>()
Expand Down Expand Up @@ -350,12 +367,15 @@ watch(
.oc-input {
@apply inline-block align-middle text-role-on-surface bg-role-surface w-full max-w-full rounded-sm p-1.5 leading-4 border border-role-outline outline-0 overflow-visible appearance-none;
}

.oc-input:focus {
@apply border border-role-surface outline-2 outline-role-outline;
}

.oc-input:disabled {
@apply bg-role-surface-container cursor-not-allowed;
}

.oc-input::placeholder {
@apply text-role-on-surface opacity-100;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

exports[`MembersPanel > should display an empty result if no matching members found 1`] = `
"<div class="ml-2">
<oc-text-input-stub label="Filter members" id="oc-textinput-3" type="text" modelvalue="no-match" clearbuttonenabled="false" clearbuttonaccessiblelabel="" disabled="false" errormessagedebouncedtime="500" fixmessageline="false" readonly="false" requiredmark="false" passwordpolicy="[object Object]" class="mr-2 mt-4"></oc-text-input-stub>
<oc-text-input-stub label="Filter members" id="oc-textinput-3" type="text" modelvalue="no-match" clearbuttonenabled="false" clearbuttonaccessiblelabel="" disabled="false" inlinelabel="false" errormessagedebouncedtime="500" fixmessageline="false" readonly="false" requiredmark="false" passwordpolicy="[object Object]" hasborder="true" class="mr-2 mt-4"></oc-text-input-stub>
<div data-testid="space-members">
<div>
<h3 class="font-semibold text-base">No members found</h3>
Expand All @@ -14,7 +14,7 @@ exports[`MembersPanel > should display an empty result if no matching members fo

exports[`MembersPanel > should render all members accordingly to their role assignments 1`] = `
"<div class="ml-2">
<oc-text-input-stub label="Filter members" id="oc-textinput-1" type="text" modelvalue="" clearbuttonenabled="false" clearbuttonaccessiblelabel="" disabled="false" errormessagedebouncedtime="500" fixmessageline="false" readonly="false" requiredmark="false" passwordpolicy="[object Object]" class="mr-2 mt-4"></oc-text-input-stub>
<oc-text-input-stub label="Filter members" id="oc-textinput-1" type="text" modelvalue="" clearbuttonenabled="false" clearbuttonaccessiblelabel="" disabled="false" inlinelabel="false" errormessagedebouncedtime="500" fixmessageline="false" readonly="false" requiredmark="false" passwordpolicy="[object Object]" hasborder="true" class="mr-2 mt-4"></oc-text-input-stub>
<div data-testid="space-members">
<!--v-if-->
<div class="mb-4" data-testid="group-members">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ exports[`EditPanel > renders all available inputs 1`] = `
<user-info-box-stub user="[object Object]"></user-info-box-stub>
<form id="user-edit-form" class="bg-role-surface-container p-4 rounded-sm" autocomplete="off">
<div>
<oc-text-input-stub label="User name" errormessage="" id="userName-input" type="text" modelvalue="" clearbuttonenabled="false" clearbuttonaccessiblelabel="" disabled="false" errormessagedebouncedtime="500" fixmessageline="true" readonly="false" requiredmark="true" passwordpolicy="[object Object]" class="mb-2"></oc-text-input-stub>
<oc-text-input-stub label="First and last name" errormessage="" id="displayName-input" type="text" modelvalue="jan" clearbuttonenabled="false" clearbuttonaccessiblelabel="" disabled="false" errormessagedebouncedtime="500" fixmessageline="true" readonly="false" requiredmark="true" passwordpolicy="[object Object]" class="mb-2"></oc-text-input-stub>
<oc-text-input-stub label="Email" errormessage="" id="email-input" type="email" modelvalue="jan@opencloud.eu" clearbuttonenabled="false" clearbuttonaccessiblelabel="" disabled="false" errormessagedebouncedtime="1000" fixmessageline="true" readonly="false" requiredmark="true" passwordpolicy="[object Object]" class="mb-2"></oc-text-input-stub>
<oc-text-input-stub label="Password" id="password-input" type="password" modelvalue="" clearbuttonenabled="false" clearbuttonaccessiblelabel="" disabled="false" errormessagedebouncedtime="500" fixmessageline="true" readonly="false" requiredmark="false" passwordpolicy="[object Object]" class="mb-2" placeholder="●●●●●●●●"></oc-text-input-stub>
<oc-text-input-stub label="User name" errormessage="" id="userName-input" type="text" modelvalue="" clearbuttonenabled="false" clearbuttonaccessiblelabel="" disabled="false" inlinelabel="false" errormessagedebouncedtime="500" fixmessageline="true" readonly="false" requiredmark="true" passwordpolicy="[object Object]" hasborder="true" class="mb-2"></oc-text-input-stub>
<oc-text-input-stub label="First and last name" errormessage="" id="displayName-input" type="text" modelvalue="jan" clearbuttonenabled="false" clearbuttonaccessiblelabel="" disabled="false" inlinelabel="false" errormessagedebouncedtime="500" fixmessageline="true" readonly="false" requiredmark="true" passwordpolicy="[object Object]" hasborder="true" class="mb-2"></oc-text-input-stub>
<oc-text-input-stub label="Email" errormessage="" id="email-input" type="email" modelvalue="jan@opencloud.eu" clearbuttonenabled="false" clearbuttonaccessiblelabel="" disabled="false" inlinelabel="false" errormessagedebouncedtime="1000" fixmessageline="true" readonly="false" requiredmark="true" passwordpolicy="[object Object]" hasborder="true" class="mb-2"></oc-text-input-stub>
<oc-text-input-stub label="Password" id="password-input" type="password" modelvalue="" clearbuttonenabled="false" clearbuttonaccessiblelabel="" disabled="false" inlinelabel="false" errormessagedebouncedtime="500" fixmessageline="true" readonly="false" requiredmark="false" passwordpolicy="[object Object]" hasborder="true" class="mb-2" placeholder="●●●●●●●●"></oc-text-input-stub>
<div class="mb-2">
<oc-select-stub label="Role" id="role-input" filter="[Function]" disabled="false" labelhidden="false" optionlabel="displayName" searchable="true" clearable="false" loading="false" fixmessageline="false" multiple="false" readonly="false" positionfixed="false" requiredmark="true" options="[object Object]"></oc-select-stub>
<oc-select-stub label="Role" id="role-input" filter="[Function]" disabled="false" inlinelabel="false" labelhidden="false" optionlabel="displayName" searchable="true" clearable="false" loading="false" fixmessageline="false" multiple="false" readonly="false" positionfixed="false" requiredmark="true" hasborder="true" options="[object Object]"></oc-select-stub>
<div class="oc-text-input-message"></div>
</div>
<div class="mb-2">
<oc-select-stub label="Login" id="login-input" filter="[Function]" disabled="false" labelhidden="false" optionlabel="label" searchable="true" clearable="false" loading="false" fixmessageline="false" multiple="false" readonly="false" positionfixed="false" requiredmark="true" model-value="[object Object]" options="[object Object],[object Object]"></oc-select-stub>
<oc-select-stub label="Login" id="login-input" filter="[Function]" disabled="false" inlinelabel="false" labelhidden="false" optionlabel="label" searchable="true" clearable="false" loading="false" fixmessageline="false" multiple="false" readonly="false" positionfixed="false" requiredmark="true" hasborder="true" model-value="[object Object]" options="[object Object],[object Object]"></oc-select-stub>
<div class="oc-text-input-message"></div>
</div>
<quota-select-stub totalquota="0" maxquota="0" id="quota-select-form" disabled="false" class="mb-2" label="Personal quota" fix-message-line="true" description-message="" read-only="false" required-mark=""></quota-select-stub>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

exports[`GroupSelect > renders the select input 1`] = `
"<div id="user-group-select-form">
<oc-select-stub label="Groups" id="oc-select-1" filter="[Function]" disabled="false" labelhidden="false" optionlabel="displayName" searchable="true" clearable="false" loading="false" fixmessageline="true" multiple="true" readonly="false" positionfixed="false" requiredmark="false" model-value="[object Object]" class="mb-2" options="undefined"></oc-select-stub>
<oc-select-stub label="Groups" id="oc-select-1" filter="[Function]" disabled="false" inlinelabel="false" labelhidden="false" optionlabel="displayName" searchable="true" clearable="false" loading="false" fixmessageline="true" multiple="true" readonly="false" positionfixed="false" requiredmark="false" hasborder="true" model-value="[object Object]" class="mb-2" options="undefined"></oc-select-stub>
</div>"
`;
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

exports[`LoginModal > renders the input including two options 1`] = `
"<div>
<oc-select-stub label="Login" descriptionmessage="" id="oc-select-1" filter="[Function]" disabled="false" labelhidden="false" optionlabel="label" searchable="true" clearable="false" loading="false" fixmessageline="false" multiple="false" readonly="false" positionfixed="true" requiredmark="true" options="[object Object],[object Object]" placeholder="Select..."></oc-select-stub>
<oc-select-stub label="Login" descriptionmessage="" id="oc-select-1" filter="[Function]" disabled="false" inlinelabel="false" labelhidden="false" optionlabel="label" searchable="true" clearable="false" loading="false" fixmessageline="false" multiple="false" readonly="false" positionfixed="true" requiredmark="true" hasborder="true" options="[object Object],[object Object]" placeholder="Select..."></oc-select-stub>
</div>"
`;
22 changes: 16 additions & 6 deletions packages/web-app-mail/src/views/MailCompose.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
</oc-button>
</div>
<div class="px-4">
<div class="py-3">
<div class="py-2 mb-2 border-b border-role-outline-variant">
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The border styling classes 'py-2 mb-2 border-b border-role-outline-variant' are duplicated across multiple form fields (lines 20, 36, 44, 52, 59). Consider extracting this repeated pattern into a CSS class or applying the border through the parent container to reduce duplication.

Copilot uses AI. Check for mistakes.
<oc-select
v-model="from"
:label="`${$gettext('From')}:`"
:inline-label="true"
:has-border="false"
:options="fromOptions"
option-label="label"
option-value="value"
Expand All @@ -31,25 +33,33 @@
<oc-text-input
v-model="toMail"
type="email"
class="mail-new-message-to-input mb-2"
class="mail-new-message-to-input mb-2 pb-2 border-b border-role-outline-variant"
:label="`${$gettext('To')}:`"
:inline-label="true"
:has-border="false"
/>
<oc-text-input
v-model="ccMail"
type="email"
class="mail-new-message-cc-input mb-2"
class="mail-new-message-cc-input mb-2 pb-2 border-b border-role-outline-variant"
:label="`${$gettext('CC')}:`"
:inline-label="true"
:has-border="false"
/>
<oc-text-input
v-model="bccMail"
type="email"
class="mail-new-message-bcc-input mb-2"
class="mail-new-message-bcc-input mb-2 pb-2 border-b border-role-outline-variant"
:label="`${$gettext('BCC')}:`"
:inline-label="true"
:has-border="false"
/>
<oc-text-input
v-model="subject"
class="mail-new-message-to-input"
:label="$gettext('Subject')"
class="mail-new-message-to-input pb-2 border-b border-role-outline-variant"
:label="`${$gettext('Subject')}:`"
:inline-label="true"
:has-border="false"
/>
<div class="py-4">
<oc-textarea v-model="mailBody" :label="$gettext('Write email')" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

exports[`resolvePublicLink > password required form > should display if password is required 1`] = `
"<form>
<oc-text-input-stub label="Enter password for public link" id="oc-textinput-2" type="password" modelvalue="" clearbuttonenabled="false" clearbuttonaccessiblelabel="" disabled="false" errormessagedebouncedtime="500" fixmessageline="false" readonly="false" requiredmark="false" passwordpolicy="[object Object]" class="mb-2 [&amp;_.oc-text-input-message]:justify-center"></oc-text-input-stub>
<oc-text-input-stub label="Enter password for public link" id="oc-textinput-2" type="password" modelvalue="" clearbuttonenabled="false" clearbuttonaccessiblelabel="" disabled="false" inlinelabel="false" errormessagedebouncedtime="500" fixmessageline="false" readonly="false" requiredmark="false" passwordpolicy="[object Object]" hasborder="true" class="mb-2 [&amp;_.oc-text-input-message]:justify-center"></oc-text-input-stub>
<oc-button-stub appearance="filled" colorrole="secondary" disabled="true" gapsize="medium" justifycontent="center" showspinner="false" size="medium" submit="submit" type="button" nohover="false" class="oc-login-authorize-button"></oc-button-stub>
</form>"
`;
Expand Down