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: accessibility improvements #11

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
14 changes: 13 additions & 1 deletion src/components/smart/AutoComplete.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,25 @@
@click="updateSuggestions"
@keydown="handleKeystroke"
@change="emit('change', $event)"
role="combobox"
aria-autocomplete="list"
aria-haspopup="true"
aria-expanded="false"
aria-controls="autocomplete-suggestions"
/>
<ul v-if="suggestions.length > 0 && suggestionsVisible" class="suggestions">
<ul
v-if="suggestions.length > 0 && suggestionsVisible"
class="suggestions"
role="listbox"
id="autocomplete-suggestions"
>
<li
v-for="(suggestion, index) in suggestions"
:key="`suggestion-${index}`"
:class="{ active: currentSuggestionIndex === index }"
@click.prevent="forceSuggestion(suggestion)"
role="option"
:aria-selected="currentSuggestionIndex === index"
>
{{ suggestion }}
</li>
Expand Down
33 changes: 20 additions & 13 deletions src/components/smart/Checkbox.vue
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
<template>
<div
class="inline-flex items-center justify-center cursor-pointer group flex-nowrap transition hover:text-secondaryDark"
class="group inline-flex cursor-pointer flex-nowrap items-center justify-center transition hover:text-secondaryDark"
role="checkbox"
:aria-checked="on"
@click="emit('change')"
@click="toggleCheckbox"
@keydown.space.prevent="toggleCheckbox"
tabindex="0"
>
<input
:id="checkboxID"
type="checkbox"
:name="name"
class="checkbox"
:checked="on"
@change="emit('change')"
@change="toggleCheckbox"
/>
<label
:for="checkboxID"
class="pl-0 font-semibold truncate align-middle cursor-pointer"
class="cursor-pointer truncate pl-0 align-middle font-semibold"
>
<slot></slot>
</label>
Expand All @@ -36,20 +38,25 @@ let checkboxIDCounter = 564275
<script setup lang="ts">
// Unique ID for checkbox
const checkboxID = `checkbox-${checkboxIDCounter++}`
defineProps({
on: {
type: Boolean,
default: false,
},
name: {
type: String,
default: "checkbox",

withDefaults(
defineProps<{
on: boolean
name: string
}>(),
{
on: false,
name: "checkbox",
},
})
)

const emit = defineEmits<{
(e: "change"): void
}>()

const toggleCheckbox = () => {
emit("change")
}
</script>

<style lang="scss" scoped>
Expand Down
7 changes: 5 additions & 2 deletions src/components/smart/Input.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
autocomplete="off"
required
:disabled="disabled"
:aria-label="label"
:aria-required="required"
/>

<label v-if="label.length > 0" :for="inputID"> {{ label }} </label>
Expand Down Expand Up @@ -56,6 +58,7 @@ const props = withDefaults(
label: string
disabled: boolean
autofocus: boolean
required?: boolean
}>(),
{
id: "",
Expand All @@ -67,7 +70,7 @@ const props = withDefaults(
label: "",
disabled: false,
autofocus: true,
}
},
)

const emit = defineEmits<{
Expand All @@ -84,6 +87,6 @@ onKeyStroke(
return emit("submit")
}
},
{ target: inputRef, eventName: "keydown" }
{ target: inputRef, eventName: "keydown" },
)
</script>
137 changes: 48 additions & 89 deletions src/components/smart/Item.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
:to="to"
:exact="exact"
:blank="blank"
class="inline-flex items-center flex-shrink-0 px-4 py-2 rounded transition hover:bg-primaryDark hover:text-secondaryDark focus:outline-none focus-visible:bg-primaryDark focus-visible:text-secondaryDark"
class="inline-flex flex-shrink-0 items-center rounded px-4 py-2 transition hover:bg-primaryDark hover:text-secondaryDark focus:outline-none focus-visible:bg-primaryDark focus-visible:text-secondaryDark"
:class="[
{ 'opacity-75 cursor-not-allowed': disabled },
{ 'cursor-not-allowed opacity-75': disabled },
{ 'pointer-events-none': loading },
{ 'flex-1': label },
{ 'flex-row-reverse justify-end': reverse },
Expand All @@ -26,20 +26,22 @@
<component
:is="icon"
v-if="icon"
class="opacity-75 svg-icons"
class="svg-icons opacity-75"
:class="[
label ? (reverse ? 'ml-4' : 'mr-4') : '',
{ 'text-accent': active },
]"
aria-hidden="true"
/>
</span>
<HoppSmartSpinner v-else class="mr-4 text-secondaryDark" />
<div
class="inline-flex items-start flex-1 truncate"
class="inline-flex flex-1 items-start truncate"
:class="{ 'flex-col': description }"
>
<div class="font-semibold truncate max-w-[16rem]">
{{ label }}
<div class="max-w-[16rem] truncate font-semibold">
<span v-if="label">{{ label }}</span>
<!-- Added span to ensure label text is announced separately -->
</div>
<p v-if="description" class="my-2 text-left text-secondaryLight">
{{ description }}
Expand All @@ -48,103 +50,60 @@
<component
:is="infoIcon"
v-if="infoIcon"
class="items-center self-center ml-6 -mr-2 svg-icons"
class="svg-icons -mr-2 ml-6 items-center self-center"
:class="{ 'text-accent': activeInfoIcon }"
aria-hidden="true"
/>
<div v-if="shortcut.length" class="ml-4 inline-flex <sm:hidden font-medium">
<div v-if="shortcut.length" class="ml-4 inline-flex font-medium <sm:hidden">
<kbd
v-for="(key, index) in shortcut"
:key="`key-${index}`"
class="-mr-2 shortcut-key"
class="shortcut-key -mr-2"
>
{{ key }}
</kbd>
</div>
</HoppSmartLink>
</template>

<script lang="ts">
<script setup lang="ts">
import HoppSmartLink from "./Link.vue"
import HoppSmartSpinner from "./Spinner.vue"
import { defineComponent } from "vue"

export default defineComponent({
components: {
HoppSmartLink,
HoppSmartSpinner,
withDefaults(
defineProps<{
to: string
exact: boolean
blank: boolean
label: string
description: string
icon: object | null
svg: object | null
disabled: boolean
loading: boolean
reverse: boolean
outline: boolean
shortcut: string[]
active: boolean
activeInfoIcon: boolean
infoIcon: object | null
}>(),
{
to: "",
exact: true,
blank: false,
label: "",
description: "",
icon: null,
svg: null,
disabled: false,
loading: false,
reverse: false,
outline: false,
shortcut: [],
active: false,
activeInfoIcon: false,
infoIcon: null,
},
props: {
to: {
type: String,
default: "",
},
exact: {
type: Boolean,
default: true,
},
blank: {
type: Boolean,
default: false,
},
label: {
type: String,
default: "",
},
description: {
type: String,
default: "",
},
/**
* This will be a component!
*/
icon: {
type: Object,
default: null,
},
/**
* This will be a component!
*/
svg: {
type: Object,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
loading: {
type: Boolean,
default: false,
},
reverse: {
type: Boolean,
default: false,
},
outline: {
type: Boolean,
default: false,
},
shortcut: {
type: Array,
default: () => [],
},
active: {
type: Boolean,
default: false,
},

activeInfoIcon: {
type: Boolean,
default: false,
},

/**
* This will be a component!
*/
infoIcon: {
type: Object,
default: null,
},
},
})
)
</script>
47 changes: 33 additions & 14 deletions src/components/smart/Link.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
<template>
<button v-if="renderedTag === 'BUTTON'" aria-label="button" role="button" v-bind="$attrs">
<button
v-if="renderedTag === 'BUTTON'"
aria-label="button"
role="button"
v-bind="$attrs"
:tabindex="$attrs.disabled ? -1 : 0"
>
<slot></slot>
</button>
<a v-else-if="renderedTag === 'ANCHOR' && !blank" aria-label="Link" :href="to" role="link" v-bind="updatedAttrs">
<a
v-else-if="renderedTag === 'ANCHOR' && !blank"
:aria-label="`Internal Link: ${to}`"
:href="to"
role="link"
v-bind="updatedAttrs"
>
<slot></slot>
</a>
<a v-else-if="renderedTag === 'ANCHOR' && blank" aria-label="Link" :href="to" role="link" target="_blank" rel="noopener"
v-bind="updatedAttrs">
<a
v-else-if="renderedTag === 'ANCHOR' && blank"
:aria-label="`External Link: ${to}`"
:href="to"
role="link"
target="_blank"
rel="noopener"
v-bind="updatedAttrs"
>
<slot></slot>
</a>
<RouterLink v-else :to="to" v-bind="updatedAttrs">
Expand All @@ -30,16 +49,16 @@ export default {
import { computed, useAttrs } from "vue"
import { omit } from "lodash-es"

const props = defineProps({
to: {
type: String,
default: "",
const props = withDefaults(
defineProps<{
to: string
blank: boolean
}>(),
{
to: "",
blank: false,
},
blank: {
type: Boolean,
default: false,
},
})
)

const renderedTag = computed(() => {
if (!props.to) {
Expand Down Expand Up @@ -67,6 +86,6 @@ const $attrs = useAttrs()
const updatedAttrs = computed(() =>
renderedTag.value === "ANCHOR" && !$attrs.disabled
? omit($attrs, "disabled")
: $attrs
: $attrs,
)
</script>
Loading