willba-component-library 0.2.101 → 0.3.0
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.
- package/.nvmrc +1 -1
- package/.storybook/main.ts +4 -6
- package/.storybook/preview.ts +1 -1
- package/README.md +21 -1
- package/lib/components/FilterBar/FilterBar.d.ts +1 -1
- package/lib/components/FilterBar/FilterBarTypes.d.ts +12 -2
- package/lib/components/FilterBar/components/buttons/select-button/SelectButton.d.ts +3 -1
- package/lib/components/FilterBar/components/cards/image-card/ImageCard.d.ts +11 -0
- package/lib/components/FilterBar/components/cards/index.d.ts +1 -0
- package/lib/components/FilterBar/components/guests/GuestCount/GuestCount.d.ts +2 -2
- package/lib/components/FilterBar/components/guests/Guests.d.ts +1 -0
- package/lib/components/FilterBar/components/index.d.ts +1 -0
- package/lib/components/FilterBar/components/locations/Locations.d.ts +14 -0
- package/lib/components/FilterBar/hooks/useFilterBar.d.ts +8 -4
- package/lib/components/FilterBar/hooks/useScrollInToView.d.ts +0 -1
- package/lib/components/FilterBar/utils/index.d.ts +1 -0
- package/lib/components/FilterBar/utils/parseLocations.d.ts +9 -0
- package/lib/components/FilterCalendar/components/Footer.d.ts +2 -2
- package/lib/components/FilterCalendar/hooks/useFilterCalendar.d.ts +0 -1
- package/lib/core/components/calendar/CalendarTypes.d.ts +1 -0
- package/lib/core/components/calendar/utils/disabledDatesByPage.d.ts +2 -2
- package/lib/core/hooks/index.d.ts +1 -0
- package/lib/core/hooks/useAutoFocus.d.ts +1 -0
- package/lib/core/hooks/useCloseFilterSection.d.ts +0 -1
- package/lib/index.d.ts +12 -2
- package/lib/index.esm.js +3890 -3481
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +3958 -3567
- package/lib/index.js.map +1 -1
- package/lib/index.umd.js +3960 -3570
- package/lib/index.umd.js.map +1 -1
- package/package.json +22 -22
- package/rollup.config.mjs +1 -0
- package/src/components/FilterBar/FilterBar.css +11 -10
- package/src/components/FilterBar/FilterBar.stories.tsx +66 -9
- package/src/components/FilterBar/FilterBar.tsx +101 -25
- package/src/components/FilterBar/FilterBarTypes.ts +12 -1
- package/src/components/FilterBar/components/buttons/select-button/SelectButton.tsx +28 -21
- package/src/components/FilterBar/components/cards/image-card/ImageCard.css +25 -0
- package/src/components/FilterBar/components/cards/image-card/ImageCard.tsx +45 -0
- package/src/components/FilterBar/components/cards/index.ts +1 -0
- package/src/components/FilterBar/components/guests/GuestCount/GuestCount.tsx +3 -3
- package/src/components/FilterBar/components/guests/Guests.tsx +9 -3
- package/src/components/FilterBar/components/index.ts +1 -0
- package/src/components/FilterBar/components/locations/Locations.css +32 -0
- package/src/components/FilterBar/components/locations/Locations.tsx +86 -0
- package/src/components/FilterBar/hooks/useFilterBar.tsx +25 -8
- package/src/components/FilterBar/utils/index.tsx +1 -0
- package/src/components/FilterBar/utils/parseGuests.tsx +7 -6
- package/src/components/FilterBar/utils/parseLocations.tsx +29 -0
- package/src/core/components/calendar/Calendar.tsx +5 -1
- package/src/core/components/calendar/CalendarTypes.ts +1 -0
- package/src/core/hooks/index.ts +1 -0
- package/src/core/hooks/useAutoFocus.tsx +27 -0
- package/src/locales/en/filterBar.json +6 -0
- package/src/locales/fi/filterBar.json +6 -0
- package/tsconfig.json +1 -1
- package/lib/components/Button/Button.stories.d.ts +0 -7
- package/lib/components/FilterBar/FilterBar.stories.d.ts +0 -6
- package/lib/components/FilterCalendar/FilterCalendar.stories.d.ts +0 -8
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import React, { forwardRef } from 'react'
|
|
2
2
|
import { useTranslation } from 'react-i18next'
|
|
3
3
|
|
|
4
|
-
import
|
|
4
|
+
import { useAutoFocus } from '../../../../core/hooks'
|
|
5
|
+
import { GuestCount } from './GuestCount/GuestCount'
|
|
5
6
|
import { AgeCategoryCount, AgeCategoryType } from '../../FilterBarTypes'
|
|
6
7
|
|
|
7
8
|
import './Guests.css'
|
|
@@ -10,16 +11,19 @@ type Props = {
|
|
|
10
11
|
ageCategories: AgeCategoryType[]
|
|
11
12
|
updateGuestsCount: (arg1: string, arg2: number) => void
|
|
12
13
|
ageCategoryCounts: AgeCategoryCount
|
|
14
|
+
autoFocus?: boolean
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export const Guests = forwardRef<HTMLDivElement, Props>(
|
|
16
|
-
({ ageCategories, updateGuestsCount, ageCategoryCounts }: Props, ref) => {
|
|
18
|
+
({ ageCategories, updateGuestsCount, ageCategoryCounts, autoFocus }: Props, ref) => {
|
|
17
19
|
const { t } = useTranslation('filterBar')
|
|
20
|
+
const containerRef = useAutoFocus<HTMLDivElement>(autoFocus)
|
|
21
|
+
|
|
18
22
|
return (
|
|
19
23
|
<div className="will-filter-bar-guests" ref={ref}>
|
|
20
24
|
<h3 className="will-guests-filter-title">{t('guests.title')}</h3>
|
|
21
25
|
|
|
22
|
-
<div className="will-guests-filter-container">
|
|
26
|
+
<div className="will-guests-filter-container" ref={containerRef}>
|
|
23
27
|
{ageCategories?.map((category) => (
|
|
24
28
|
<GuestCount
|
|
25
29
|
key={category.id}
|
|
@@ -36,3 +40,5 @@ export const Guests = forwardRef<HTMLDivElement, Props>(
|
|
|
36
40
|
)
|
|
37
41
|
}
|
|
38
42
|
)
|
|
43
|
+
|
|
44
|
+
Guests.displayName = 'Guests'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
.will-filter-bar-locations {
|
|
2
|
+
text-align: initial;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.will-locations-filter-title {
|
|
6
|
+
font-size: 22px;
|
|
7
|
+
margin: 10px 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.will-locations-filter-subtitle {
|
|
11
|
+
font-size: 15px;
|
|
12
|
+
font-weight: 500;
|
|
13
|
+
color: var(--will-text);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.will-locations-filter-container {
|
|
17
|
+
display: flex;
|
|
18
|
+
gap: 10px;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
min-width: 400px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@media (max-width: 960px) {
|
|
24
|
+
.will-locations-filter-title {
|
|
25
|
+
font-size: 18px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.will-locations-filter-container {
|
|
29
|
+
margin-top: 15px;
|
|
30
|
+
min-width: 100%;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React, { forwardRef, useEffect, useRef } from 'react'
|
|
2
|
+
import { useTranslation } from 'react-i18next'
|
|
3
|
+
|
|
4
|
+
import './Locations.css'
|
|
5
|
+
import { ImageCard } from '../cards/image-card/ImageCard'
|
|
6
|
+
import { Location } from '../../FilterBarTypes'
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
locations?: Location[]
|
|
10
|
+
language?: string
|
|
11
|
+
selectedLocations: Location[]
|
|
12
|
+
setSelectedLocations: (locations: Location[]) => void
|
|
13
|
+
autoFocus?: boolean
|
|
14
|
+
multiSelect?: boolean
|
|
15
|
+
onClose?: () => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const Locations = forwardRef<HTMLDivElement, Props>(
|
|
19
|
+
(
|
|
20
|
+
{
|
|
21
|
+
locations,
|
|
22
|
+
language,
|
|
23
|
+
selectedLocations,
|
|
24
|
+
setSelectedLocations,
|
|
25
|
+
autoFocus,
|
|
26
|
+
multiSelect = false,
|
|
27
|
+
onClose,
|
|
28
|
+
},
|
|
29
|
+
ref
|
|
30
|
+
) => {
|
|
31
|
+
const { t } = useTranslation('filterBar')
|
|
32
|
+
const firstCardRef = useRef<HTMLDivElement>(null)
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (autoFocus && firstCardRef.current) {
|
|
36
|
+
firstCardRef.current.focus()
|
|
37
|
+
}
|
|
38
|
+
}, [autoFocus])
|
|
39
|
+
|
|
40
|
+
const handleLocationClick = (location: Location) => {
|
|
41
|
+
if (multiSelect) {
|
|
42
|
+
// Multi-select: toggle location in array
|
|
43
|
+
const isSelected = selectedLocations.some((loc) => loc.id === location.id)
|
|
44
|
+
|
|
45
|
+
if (isSelected) {
|
|
46
|
+
// Remove location if already selected
|
|
47
|
+
setSelectedLocations(
|
|
48
|
+
selectedLocations.filter((loc) => loc.id !== location.id)
|
|
49
|
+
)
|
|
50
|
+
} else {
|
|
51
|
+
// Add location to selection
|
|
52
|
+
setSelectedLocations([...selectedLocations, location])
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
// Single-select: replace selection with clicked location and close modal
|
|
56
|
+
setSelectedLocations([location])
|
|
57
|
+
onClose?.()
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className="will-filter-bar-locations" ref={ref}>
|
|
63
|
+
<h3 className="will-locations-filter-title">{t('locations.title')}</h3>
|
|
64
|
+
|
|
65
|
+
<div className="will-locations-filter-container">
|
|
66
|
+
{!!(locations?.length && language) &&
|
|
67
|
+
locations
|
|
68
|
+
.filter((location) => location?.label?.[language])
|
|
69
|
+
.map((location, index) => (
|
|
70
|
+
<ImageCard
|
|
71
|
+
key={location.id}
|
|
72
|
+
ref={index === 0 ? firstCardRef : null}
|
|
73
|
+
title={location.label[language]}
|
|
74
|
+
description={location.description?.[language]}
|
|
75
|
+
imageUrl={location.imageUrl}
|
|
76
|
+
isSelected={selectedLocations.some((loc) => loc.id === location.id)}
|
|
77
|
+
onClick={() => handleLocationClick(location)}
|
|
78
|
+
/>
|
|
79
|
+
))}
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
Locations.displayName = 'Locations'
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
Filters,
|
|
9
9
|
Pages,
|
|
10
10
|
Tab,
|
|
11
|
+
Location,
|
|
11
12
|
} from '../FilterBarTypes'
|
|
12
13
|
|
|
13
14
|
type Props = {
|
|
@@ -15,7 +16,10 @@ type Props = {
|
|
|
15
16
|
ageCategories?: AgeCategoryType[]
|
|
16
17
|
onSubmit?: ((val: Filters) => void) | null
|
|
17
18
|
tabs?: Tab[]
|
|
18
|
-
|
|
19
|
+
locations?: {
|
|
20
|
+
multiSelect: boolean
|
|
21
|
+
data: Location[]
|
|
22
|
+
}
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
export const useFilterBar = ({
|
|
@@ -23,7 +27,7 @@ export const useFilterBar = ({
|
|
|
23
27
|
ageCategories,
|
|
24
28
|
onSubmit,
|
|
25
29
|
tabs,
|
|
26
|
-
|
|
30
|
+
locations,
|
|
27
31
|
}: Props) => {
|
|
28
32
|
const [selectedPath, setSelectedPath] = useState<string>(Pages.EVENTS)
|
|
29
33
|
const [selectedFilter, setSelectedFilter] = useState<string | boolean>(false)
|
|
@@ -34,12 +38,15 @@ export const useFilterBar = ({
|
|
|
34
38
|
const [ageCategoryCounts, setAgeCategoryCounts] = useState<AgeCategoryCount>(
|
|
35
39
|
{}
|
|
36
40
|
)
|
|
41
|
+
const [selectedLocations, setSelectedLocations] = useState<Location[]>([])
|
|
37
42
|
|
|
38
43
|
useEffect(() => {
|
|
39
44
|
const urlSearchParams = new URLSearchParams(window.location.search)
|
|
40
45
|
|
|
41
46
|
const startDateParam = urlSearchParams.get('startDate')
|
|
42
47
|
const endDateParam = urlSearchParams.get('endDate')
|
|
48
|
+
// Get all locationId params from URL
|
|
49
|
+
const locationIdParams = urlSearchParams.getAll('locationId')
|
|
43
50
|
const ageCategoryCountsParam = JSON.parse(
|
|
44
51
|
urlSearchParams.get('ageCategoryCounts') || '{}'
|
|
45
52
|
)
|
|
@@ -54,8 +61,17 @@ export const useFilterBar = ({
|
|
|
54
61
|
to: new Date(endDateParam),
|
|
55
62
|
})
|
|
56
63
|
}
|
|
64
|
+
|
|
57
65
|
setAgeCategoryCounts(ageCategoryCountsParam)
|
|
58
66
|
setCategories(parsedCategories)
|
|
67
|
+
|
|
68
|
+
// Set selected locations from URL - handle all locationIds
|
|
69
|
+
if (locations?.data?.length && locationIdParams.length) {
|
|
70
|
+
const matchedLocations = locations.data.filter((location) =>
|
|
71
|
+
locationIdParams.includes(location.id)
|
|
72
|
+
)
|
|
73
|
+
setSelectedLocations(matchedLocations)
|
|
74
|
+
}
|
|
59
75
|
}, [])
|
|
60
76
|
|
|
61
77
|
useEffect(() => {
|
|
@@ -112,6 +128,11 @@ export const useFilterBar = ({
|
|
|
112
128
|
}
|
|
113
129
|
}
|
|
114
130
|
|
|
131
|
+
// Append all selected locationIds
|
|
132
|
+
selectedLocations.forEach((location) => {
|
|
133
|
+
querySearchParams.append('locationId', location.id)
|
|
134
|
+
})
|
|
135
|
+
|
|
115
136
|
handleSelectedFilter(false)
|
|
116
137
|
|
|
117
138
|
if (onSubmit && window.location.href.includes(selectedPath)) {
|
|
@@ -119,12 +140,6 @@ export const useFilterBar = ({
|
|
|
119
140
|
} else {
|
|
120
141
|
const params = new URLSearchParams(querySearchParams ?? undefined)
|
|
121
142
|
|
|
122
|
-
if (locationIds) {
|
|
123
|
-
locationIds.forEach((id) => {
|
|
124
|
-
params.append('locationId', id)
|
|
125
|
-
})
|
|
126
|
-
}
|
|
127
|
-
|
|
128
143
|
const paramString = params.toString()
|
|
129
144
|
const path = `${redirectUrl}${selectedPath}`
|
|
130
145
|
|
|
@@ -145,6 +160,8 @@ export const useFilterBar = ({
|
|
|
145
160
|
calendarRange,
|
|
146
161
|
selectedPath,
|
|
147
162
|
innerLoading,
|
|
163
|
+
selectedLocations,
|
|
164
|
+
setSelectedLocations,
|
|
148
165
|
setCalendarRange,
|
|
149
166
|
setSelectedFilter,
|
|
150
167
|
setAgeCategoryCounts,
|
|
@@ -22,7 +22,10 @@ export const parseGuests = ({
|
|
|
22
22
|
}: Props) => {
|
|
23
23
|
const parsedData = Object.entries(ageCategoryCounts).reduce(
|
|
24
24
|
(acc: AccType, [key, value]) => {
|
|
25
|
-
const
|
|
25
|
+
const parts = key.split('-')
|
|
26
|
+
if (parts.length < 2) return acc
|
|
27
|
+
|
|
28
|
+
const ageCategoryId = parts[1]
|
|
26
29
|
const ageCategory = ageCategories.find((c) => c.id === ageCategoryId)
|
|
27
30
|
|
|
28
31
|
if (ageCategory && value) {
|
|
@@ -42,11 +45,9 @@ export const parseGuests = ({
|
|
|
42
45
|
|
|
43
46
|
return {
|
|
44
47
|
content: parsedData.total
|
|
45
|
-
? `<span
|
|
46
|
-
parsedData.total
|
|
47
|
-
}
|
|
48
|
-
htmlString ? ` ( ${htmlString} )` : ''
|
|
49
|
-
}`
|
|
48
|
+
? `<span class="will-guest-count">${parsedData.total}</span> ${
|
|
49
|
+
parsedData.total > 1 ? guestsLabel : guestLabel
|
|
50
|
+
}${htmlString ? ` ( ${htmlString} )` : ''}`
|
|
50
51
|
: guestsPlaceholder,
|
|
51
52
|
data: parsedData,
|
|
52
53
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Location } from '../FilterBarTypes'
|
|
2
|
+
|
|
3
|
+
type Props = {
|
|
4
|
+
selectedLocations: Location[]
|
|
5
|
+
language: string
|
|
6
|
+
locationsPlaceholder: string
|
|
7
|
+
locationsSelectedLabel?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const parseLocations = ({
|
|
11
|
+
selectedLocations,
|
|
12
|
+
language,
|
|
13
|
+
locationsPlaceholder,
|
|
14
|
+
locationsSelectedLabel = 'locations',
|
|
15
|
+
}: Props) => {
|
|
16
|
+
if (!selectedLocations.length) {
|
|
17
|
+
return locationsPlaceholder
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (selectedLocations.length === 1) {
|
|
21
|
+
const translation = selectedLocations[0]?.label?.[language]
|
|
22
|
+
if (!translation) {
|
|
23
|
+
return locationsPlaceholder
|
|
24
|
+
}
|
|
25
|
+
return translation
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return `${selectedLocations.length} ${locationsSelectedLabel}`
|
|
29
|
+
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
useCalendarTooltips,
|
|
20
20
|
useUpdateDisabledDates,
|
|
21
21
|
} from './hooks'
|
|
22
|
+
import { useAutoFocus } from '../../hooks'
|
|
22
23
|
|
|
23
24
|
import 'react-day-picker/dist/style.css'
|
|
24
25
|
import './Calendar.css'
|
|
@@ -43,12 +44,15 @@ export const Calendar = forwardRef<HTMLDivElement, CalendarTypes>(
|
|
|
43
44
|
setUpdatedForSubmit,
|
|
44
45
|
rangeContext,
|
|
45
46
|
calendarHasError,
|
|
47
|
+
autoFocus,
|
|
46
48
|
}: CalendarTypes,
|
|
47
49
|
ref
|
|
48
50
|
) => {
|
|
49
51
|
// Translations
|
|
50
52
|
const { t } = useTranslation('common')
|
|
51
53
|
|
|
54
|
+
const calendarContainerRef = useAutoFocus<HTMLDivElement>(autoFocus)
|
|
55
|
+
|
|
52
56
|
const isTablet = useMediaQuery({ maxWidth: 960 })
|
|
53
57
|
const today = startOfDay(new Date())
|
|
54
58
|
const selectedStartDate = calendarRange?.from
|
|
@@ -158,7 +162,7 @@ export const Calendar = forwardRef<HTMLDivElement, CalendarTypes>(
|
|
|
158
162
|
|
|
159
163
|
return (
|
|
160
164
|
<div className="will-filter-bar-calendar" ref={ref}>
|
|
161
|
-
<div className="will-calendar-filter-container">
|
|
165
|
+
<div className="will-calendar-filter-container" ref={calendarContainerRef}>
|
|
162
166
|
<DayPicker
|
|
163
167
|
key={updateCalendarDefaultMonth}
|
|
164
168
|
id="will-calendar"
|
package/src/core/hooks/index.ts
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
export const useAutoFocus = <T extends HTMLElement>(autoFocus?: boolean) => {
|
|
4
|
+
const ref = useRef<T>(null)
|
|
5
|
+
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
if (!autoFocus || !ref.current) return
|
|
8
|
+
|
|
9
|
+
const attemptFocus = (attempts = 0): void => {
|
|
10
|
+
if (attempts > 20 || !ref.current) return
|
|
11
|
+
|
|
12
|
+
const focusable = ref.current.querySelector<HTMLElement>(
|
|
13
|
+
'button:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
if (focusable) {
|
|
17
|
+
focusable.focus()
|
|
18
|
+
} else {
|
|
19
|
+
requestAnimationFrame(() => attemptFocus(attempts + 1))
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
requestAnimationFrame(() => attemptFocus())
|
|
24
|
+
}, [autoFocus])
|
|
25
|
+
|
|
26
|
+
return ref
|
|
27
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
-
import Button from "./Button";
|
|
3
|
-
declare const meta: Meta<typeof Button>;
|
|
4
|
-
export default meta;
|
|
5
|
-
type Story = StoryObj<typeof Button>;
|
|
6
|
-
export declare const Primary: Story;
|
|
7
|
-
export declare const Secondary: Story;
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
-
import FilterCalendar from './FilterCalendar';
|
|
3
|
-
declare const meta: Meta<typeof FilterCalendar>;
|
|
4
|
-
export default meta;
|
|
5
|
-
type Story = StoryObj<typeof FilterCalendar>;
|
|
6
|
-
export declare const Default: Story;
|
|
7
|
-
export declare const RangeContext: Story;
|
|
8
|
-
export declare const DisabledRangeContextDates: Story;
|