@saasmakers/ui 2.1.4 → 2.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -46,17 +46,15 @@ const id = useId()
46
46
  const modelValue = defineModel<FieldInput['modelValue']>({ default: '' })
47
47
  const input = ref<HTMLInputElement>()
48
48
  const showPassword = ref(false)
49
- const focused = ref(false)
50
- const { isDesktopBrowser, isIos } = useDevice()
49
+ const { isDesktopBrowser } = useDevice()
51
50
 
52
- const iosUppercaseTyping = computed(() => {
53
- const isProseField = props.type === 'text' || props.type === 'search'
51
+ const { isActiveOnIos, isTypingOnIos, onBlur: onIosUppercaseBlur, onCompositionEnd: onIosUppercaseCompositionEnd, onFocus: onIosUppercaseFocus, onInput: onIosUppercaseInput } = useIosUppercase({
52
+ enabled: () => {
53
+ const isProseField = props.type === 'text' || props.type === 'search'
54
54
 
55
- return props.uppercase && isIos.value && isProseField && !props.lowercaseOnly && !props.slugOnly
56
- })
57
-
58
- const hideCssUppercase = computed(() => {
59
- return iosUppercaseTyping.value && focused.value
55
+ return props.uppercase && isProseField && !props.lowercaseOnly && !props.slugOnly
56
+ },
57
+ modelValue,
60
58
  })
61
59
 
62
60
  onMounted(() => {
@@ -78,8 +76,7 @@ function focus() {
78
76
  }
79
77
 
80
78
  function onFieldBlur(event: FocusEvent) {
81
- focused.value = false
82
-
79
+ onIosUppercaseBlur()
83
80
  syncModelValueFromTarget(event.target)
84
81
 
85
82
  if (props.trim) {
@@ -90,20 +87,12 @@ function onFieldBlur(event: FocusEvent) {
90
87
  }
91
88
 
92
89
  function onFieldFocus(event: FocusEvent) {
93
- if (iosUppercaseTyping.value) {
94
- const uppercased = modelValue.value.toString().toUpperCase()
95
-
96
- if (uppercased !== modelValue.value) {
97
- modelValue.value = uppercased
98
- }
99
- }
100
-
101
- focused.value = true
102
-
90
+ onIosUppercaseFocus()
103
91
  emit('focus', event)
104
92
  }
105
93
 
106
94
  function onFieldInput(event: Event) {
95
+ onIosUppercaseInput(event)
107
96
  syncModelValueFromTarget(event.target)
108
97
  }
109
98
 
@@ -216,12 +205,12 @@ defineExpose({
216
205
  :id="id"
217
206
  ref="input"
218
207
  :autocomplete="autocomplete ? 'on' : 'off'"
219
- :autocapitalize="iosUppercaseTyping ? 'characters' : undefined"
208
+ :autocapitalize="isActiveOnIos ? 'characters' : undefined"
220
209
  v-bind="autofocus ? { 'data-autofocus': '' } : {}"
221
210
  class="h-full w-full appearance-none items-center rounded-lg font-medium tracking-tight outline-none placeholder-gray-600 dark:placeholder-gray-400"
222
211
  :class="{
223
- 'uppercase': uppercase && !hideCssUppercase,
224
- 'placeholder:uppercase': hideCssUppercase,
212
+ 'uppercase': uppercase && !isTypingOnIos,
213
+ 'placeholder:uppercase': isTypingOnIos,
225
214
  'field-disabled': disabled,
226
215
  'focus:placeholder-gray-900 hover:placeholder-gray-900 dark:focus:placeholder-gray-100 dark:hover:placeholder-gray-100': !disabled,
227
216
  'text-gray-900 dark:text-gray-100': !lineThrough,
@@ -256,6 +245,7 @@ defineExpose({
256
245
  :type="type === 'password' && showPassword ? 'text' : type"
257
246
  :value="modelValue"
258
247
  @blur="onFieldBlur"
248
+ @compositionend="onIosUppercaseCompositionEnd"
259
249
  @focus="onFieldFocus"
260
250
  @input="onFieldInput"
261
251
  @keydown="onFieldKeyDown"
@@ -30,15 +30,11 @@ const emit = defineEmits<{
30
30
  const id = useId()
31
31
  const modelValue = defineModel<FieldTextarea['modelValue']>({ default: '' })
32
32
  const textarea = ref<HTMLTextAreaElement>()
33
- const focused = ref(false)
34
- const { isDesktopBrowser, isIos } = useDevice()
33
+ const { isDesktopBrowser } = useDevice()
35
34
 
36
- const iosUppercaseTyping = computed(() => {
37
- return props.uppercase && isIos.value
38
- })
39
-
40
- const hideCssUppercase = computed(() => {
41
- return iosUppercaseTyping.value && focused.value
35
+ const { isActiveOnIos, isTypingOnIos, onBlur: onIosUppercaseBlur, onCompositionEnd: onIosUppercaseCompositionEnd, onFocus: onIosUppercaseFocus, onInput: onIosUppercaseInput } = useIosUppercase({
36
+ enabled: () => props.uppercase,
37
+ modelValue,
42
38
  })
43
39
 
44
40
  onMounted(() => {
@@ -48,8 +44,7 @@ onMounted(() => {
48
44
  })
49
45
 
50
46
  function onFieldBlur(event: FocusEvent) {
51
- focused.value = false
52
-
47
+ onIosUppercaseBlur()
53
48
  syncModelValueFromTarget(event.target)
54
49
  emit('blur', event, modelValue.value || '')
55
50
  }
@@ -58,18 +53,6 @@ function onFieldClick(event: MouseEvent) {
58
53
  emit('click', event, modelValue.value || '')
59
54
  }
60
55
 
61
- function onFieldFocus() {
62
- if (iosUppercaseTyping.value) {
63
- const uppercased = modelValue.value.toString().toUpperCase()
64
-
65
- if (uppercased !== modelValue.value) {
66
- modelValue.value = uppercased
67
- }
68
- }
69
-
70
- focused.value = true
71
- }
72
-
73
56
  function syncModelValueFromTarget(target: EventTarget | null) {
74
57
  if (!(target instanceof HTMLTextAreaElement)) {
75
58
  return
@@ -100,11 +83,11 @@ function syncModelValueFromTarget(target: EventTarget | null) {
100
83
  ref="textarea"
101
84
  v-model="modelValue"
102
85
  v-bind="{ 'data-enable-grammarly': 'false' }"
103
- :autocapitalize="iosUppercaseTyping ? 'characters' : undefined"
86
+ :autocapitalize="isActiveOnIos ? 'characters' : undefined"
104
87
  class="w-full flex-1 appearance-none rounded-lg text-gray-900 font-medium tracking-tight outline-none dark:text-gray-100 placeholder-gray-600 dark:placeholder-gray-400"
105
88
  :class="{
106
- 'uppercase': uppercase && !hideCssUppercase,
107
- 'placeholder:uppercase': hideCssUppercase,
89
+ 'uppercase': uppercase && !isTypingOnIos,
90
+ 'placeholder:uppercase': isTypingOnIos,
108
91
  'field-disabled': disabled,
109
92
  'focus:placeholder-gray-900 hover:placeholder-gray-900 dark:focus:placeholder-gray-100 dark:hover:placeholder-gray-100': !disabled,
110
93
  'px-sm py-2.5': padding && size === 'sm',
@@ -127,7 +110,9 @@ function syncModelValueFromTarget(target: EventTarget | null) {
127
110
  spellcheck="false"
128
111
  @blur="onFieldBlur"
129
112
  @click="onFieldClick"
130
- @focus="onFieldFocus"
113
+ @compositionend="onIosUppercaseCompositionEnd"
114
+ @focus="onIosUppercaseFocus"
115
+ @input="onIosUppercaseInput"
131
116
  />
132
117
 
133
118
  <FieldMessage
@@ -1,6 +1,6 @@
1
1
  export default function useDevice() {
2
2
  const { isIOS, isNative } = useCapacitor()
3
- const isIos = ref(false)
3
+ const isIosDevice = ref(false)
4
4
 
5
5
  const isDesktopBrowser = computed(() => {
6
6
  if (typeof navigator === 'undefined') {
@@ -20,24 +20,16 @@ export default function useDevice() {
20
20
  })
21
21
 
22
22
  onMounted(() => {
23
- isIos.value = detectIosDevice(isIOS.value)
23
+ isIosDevice.value = isIOS.value || isIosBrowser()
24
24
  })
25
25
 
26
26
  return {
27
27
  isDesktopBrowser,
28
- isIos,
28
+ isIosDevice,
29
29
  }
30
30
  }
31
31
 
32
- function detectIosDevice(isNativeIos: boolean) {
33
- if (isNativeIos) {
34
- return true
35
- }
36
-
37
- if (typeof navigator === 'undefined') {
38
- return false
39
- }
40
-
32
+ function isIosBrowser() {
41
33
  if (/iPad|iPhone|iPod/.test(navigator.userAgent)) {
42
34
  return true
43
35
  }
@@ -0,0 +1,75 @@
1
+ // iOS WebKit drops keyboard suggestions while `text-transform: uppercase` is active,
2
+ // so the transform is lifted during typing and the value itself is uppercased instead.
3
+ export default function useIosUppercase(options: {
4
+ enabled: MaybeRefOrGetter<boolean>
5
+ modelValue: Ref<string>
6
+ }) {
7
+ const { isIosDevice } = useDevice()
8
+ const isFocused = ref(false)
9
+
10
+ const isActiveOnIos = computed(() => {
11
+ return toValue(options.enabled) && isIosDevice.value
12
+ })
13
+
14
+ const isTypingOnIos = computed(() => {
15
+ return isActiveOnIos.value && isFocused.value
16
+ })
17
+
18
+ function onFocus() {
19
+ if (isActiveOnIos.value) {
20
+ options.modelValue.value = options.modelValue.value.toUpperCase()
21
+ }
22
+
23
+ isFocused.value = true
24
+ }
25
+
26
+ function onBlur() {
27
+ isFocused.value = false
28
+ }
29
+
30
+ function onInput(event: Event) {
31
+ // Rewriting the value mid-composition breaks IME and accent input.
32
+ if (event instanceof InputEvent && event.isComposing) {
33
+ return
34
+ }
35
+
36
+ forceUppercase(event.target)
37
+ }
38
+
39
+ function onCompositionEnd(event: CompositionEvent) {
40
+ forceUppercase(event.target)
41
+ }
42
+
43
+ function forceUppercase(target: EventTarget | null) {
44
+ if (!isActiveOnIos.value) {
45
+ return
46
+ }
47
+
48
+ if (!(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)) {
49
+ return
50
+ }
51
+
52
+ const uppercased = target.value.toUpperCase()
53
+
54
+ if (uppercased === target.value) {
55
+ return
56
+ }
57
+
58
+ const { selectionEnd, selectionStart } = target
59
+
60
+ target.value = uppercased
61
+
62
+ target.setSelectionRange(selectionStart ?? uppercased.length, selectionEnd ?? uppercased.length)
63
+
64
+ options.modelValue.value = uppercased
65
+ }
66
+
67
+ return {
68
+ isActiveOnIos,
69
+ isTypingOnIos,
70
+ onBlur,
71
+ onCompositionEnd,
72
+ onFocus,
73
+ onInput,
74
+ }
75
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saasmakers/ui",
3
- "version": "2.1.4",
3
+ "version": "2.1.6",
4
4
  "private": false,
5
5
  "description": "Reusable Nuxt UI components for SaaS Makers projects",
6
6
  "license": "MIT",