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.
Files changed (60) hide show
  1. package/.nvmrc +1 -1
  2. package/.storybook/main.ts +4 -6
  3. package/.storybook/preview.ts +1 -1
  4. package/README.md +21 -1
  5. package/lib/components/FilterBar/FilterBar.d.ts +1 -1
  6. package/lib/components/FilterBar/FilterBarTypes.d.ts +12 -2
  7. package/lib/components/FilterBar/components/buttons/select-button/SelectButton.d.ts +3 -1
  8. package/lib/components/FilterBar/components/cards/image-card/ImageCard.d.ts +11 -0
  9. package/lib/components/FilterBar/components/cards/index.d.ts +1 -0
  10. package/lib/components/FilterBar/components/guests/GuestCount/GuestCount.d.ts +2 -2
  11. package/lib/components/FilterBar/components/guests/Guests.d.ts +1 -0
  12. package/lib/components/FilterBar/components/index.d.ts +1 -0
  13. package/lib/components/FilterBar/components/locations/Locations.d.ts +14 -0
  14. package/lib/components/FilterBar/hooks/useFilterBar.d.ts +8 -4
  15. package/lib/components/FilterBar/hooks/useScrollInToView.d.ts +0 -1
  16. package/lib/components/FilterBar/utils/index.d.ts +1 -0
  17. package/lib/components/FilterBar/utils/parseLocations.d.ts +9 -0
  18. package/lib/components/FilterCalendar/components/Footer.d.ts +2 -2
  19. package/lib/components/FilterCalendar/hooks/useFilterCalendar.d.ts +0 -1
  20. package/lib/core/components/calendar/CalendarTypes.d.ts +1 -0
  21. package/lib/core/components/calendar/utils/disabledDatesByPage.d.ts +2 -2
  22. package/lib/core/hooks/index.d.ts +1 -0
  23. package/lib/core/hooks/useAutoFocus.d.ts +1 -0
  24. package/lib/core/hooks/useCloseFilterSection.d.ts +0 -1
  25. package/lib/index.d.ts +12 -2
  26. package/lib/index.esm.js +3890 -3481
  27. package/lib/index.esm.js.map +1 -1
  28. package/lib/index.js +3958 -3567
  29. package/lib/index.js.map +1 -1
  30. package/lib/index.umd.js +3960 -3570
  31. package/lib/index.umd.js.map +1 -1
  32. package/package.json +22 -22
  33. package/rollup.config.mjs +1 -0
  34. package/src/components/FilterBar/FilterBar.css +11 -10
  35. package/src/components/FilterBar/FilterBar.stories.tsx +66 -9
  36. package/src/components/FilterBar/FilterBar.tsx +101 -25
  37. package/src/components/FilterBar/FilterBarTypes.ts +12 -1
  38. package/src/components/FilterBar/components/buttons/select-button/SelectButton.tsx +28 -21
  39. package/src/components/FilterBar/components/cards/image-card/ImageCard.css +25 -0
  40. package/src/components/FilterBar/components/cards/image-card/ImageCard.tsx +45 -0
  41. package/src/components/FilterBar/components/cards/index.ts +1 -0
  42. package/src/components/FilterBar/components/guests/GuestCount/GuestCount.tsx +3 -3
  43. package/src/components/FilterBar/components/guests/Guests.tsx +9 -3
  44. package/src/components/FilterBar/components/index.ts +1 -0
  45. package/src/components/FilterBar/components/locations/Locations.css +32 -0
  46. package/src/components/FilterBar/components/locations/Locations.tsx +86 -0
  47. package/src/components/FilterBar/hooks/useFilterBar.tsx +25 -8
  48. package/src/components/FilterBar/utils/index.tsx +1 -0
  49. package/src/components/FilterBar/utils/parseGuests.tsx +7 -6
  50. package/src/components/FilterBar/utils/parseLocations.tsx +29 -0
  51. package/src/core/components/calendar/Calendar.tsx +5 -1
  52. package/src/core/components/calendar/CalendarTypes.ts +1 -0
  53. package/src/core/hooks/index.ts +1 -0
  54. package/src/core/hooks/useAutoFocus.tsx +27 -0
  55. package/src/locales/en/filterBar.json +6 -0
  56. package/src/locales/fi/filterBar.json +6 -0
  57. package/tsconfig.json +1 -1
  58. package/lib/components/Button/Button.stories.d.ts +0 -7
  59. package/lib/components/FilterBar/FilterBar.stories.d.ts +0 -6
  60. 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 GuestCount from './GuestCount/GuestCount'
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'
@@ -3,3 +3,4 @@ export { CloseButton, SelectButton, SubmitButton, TabButton } from './buttons'
3
3
  export { Guests } from './guests/Guests'
4
4
  export { Divider } from './divider/Divider'
5
5
  export { Categories } from './categories/Categories'
6
+ export { Locations } from './locations/Locations'
@@ -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
- locationIds?: string[]
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
- locationIds,
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,
@@ -1 +1,2 @@
1
1
  export { parseGuests } from './parseGuests'
2
+ export { parseLocations } from './parseLocations'
@@ -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 ageCategoryId = key[key.length - 1]
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 style="display: inline-block; min-width: 10px">${
46
- parsedData.total
47
- }</span> ${parsedData.total > 1 ? guestsLabel : guestLabel}${
48
- htmlString ? ` &nbsp; ( ${htmlString} )` : ''
49
- }`
48
+ ? `<span class="will-guest-count">${parsedData.total}</span> ${
49
+ parsedData.total > 1 ? guestsLabel : guestLabel
50
+ }${htmlString ? ` &nbsp; ( ${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"
@@ -44,4 +44,5 @@ export type CalendarTypes = {
44
44
  setUpdatedForSubmit?: (arg: boolean) => void
45
45
  rangeContext?: RangeContext
46
46
  calendarHasError?: boolean
47
+ autoFocus?: boolean
47
48
  }
@@ -1,3 +1,4 @@
1
1
  export { useAwaitRender } from './useAwaitRender'
2
2
  export { useUpdateTranslations } from './useUpdateTranslations'
3
3
  export { useCloseFilterSection } from './useCloseFilterSection'
4
+ export { useAutoFocus } from './useAutoFocus'
@@ -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
+ }
@@ -1,4 +1,10 @@
1
1
  {
2
+ "locations": {
3
+ "label": "Locations",
4
+ "title": "Where to?",
5
+ "placeholder": "Add location",
6
+ "selected": "locations"
7
+ },
2
8
  "calendar": {
3
9
  "label": "Dates",
4
10
  "roomsLabelPlaceholder": "Add check-in and check-out",
@@ -1,4 +1,10 @@
1
1
  {
2
+ "locations": {
3
+ "label": "Sijainnit",
4
+ "title": "Minne?",
5
+ "placeholder": "Lisää sijainti",
6
+ "selected": "sijaintia"
7
+ },
2
8
  "calendar": {
3
9
  "label": "Päivät",
4
10
  "roomsLabelPlaceholder": "Lisää check-in ja check-out",
package/tsconfig.json CHANGED
@@ -25,5 +25,5 @@
25
25
  }
26
26
  },
27
27
  "include": ["src"],
28
- "exclude": ["node_modules", "lib"]
28
+ "exclude": ["node_modules", "lib", "**/*.stories.tsx", "**/*.stories.ts"]
29
29
  }
@@ -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,6 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/react';
2
- import FilterBar from './FilterBar';
3
- declare const meta: Meta<typeof FilterBar>;
4
- export default meta;
5
- type Story = StoryObj<typeof FilterBar>;
6
- export declare const Main: 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;