willba-component-library 0.3.10 → 0.3.11

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 (28) hide show
  1. package/lib/components/FilterBar/FilterBar.d.ts +1 -2
  2. package/lib/components/FilterBar/FilterBarTypes.d.ts +3 -13
  3. package/lib/components/FilterBar/components/dates/Dates.d.ts +1 -0
  4. package/lib/components/FilterBar/utils/index.d.ts +1 -1
  5. package/lib/components/FilterBar/utils/parseLocations.d.ts +1 -2
  6. package/lib/index.d.ts +8 -16
  7. package/lib/index.esm.js +176 -103
  8. package/lib/index.esm.js.map +1 -1
  9. package/lib/index.js +424 -351
  10. package/lib/index.js.map +1 -1
  11. package/lib/index.umd.js +424 -351
  12. package/lib/index.umd.js.map +1 -1
  13. package/package.json +1 -1
  14. package/src/components/FilterBar/FilterBar.css +1 -1
  15. package/src/components/FilterBar/FilterBar.stories.tsx +14 -49
  16. package/src/components/FilterBar/FilterBar.tsx +44 -13
  17. package/src/components/FilterBar/FilterBarTypes.ts +3 -14
  18. package/src/components/FilterBar/components/cards/image-card/ImageCard.css +0 -1
  19. package/src/components/FilterBar/components/common/FilterSectionHeader.css +1 -0
  20. package/src/components/FilterBar/components/dates/Dates.css +3 -0
  21. package/src/components/FilterBar/components/dates/Dates.tsx +2 -0
  22. package/src/components/FilterBar/components/guests/Guests.css +1 -1
  23. package/src/components/FilterBar/components/locations/Locations.css +1 -1
  24. package/src/components/FilterBar/components/locations/Locations.tsx +15 -35
  25. package/src/components/FilterBar/utils/calculateDropdownPosition.tsx +106 -0
  26. package/src/components/FilterBar/utils/index.tsx +1 -1
  27. package/src/components/FilterBar/utils/parseLocations.tsx +3 -7
  28. package/src/components/FilterBar/utils/getLocalizedContent.tsx +0 -21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "willba-component-library",
3
- "version": "0.3.10",
3
+ "version": "0.3.11",
4
4
  "description": "A custom UI component library",
5
5
  "main": "lib/index.js",
6
6
  "module": "lib/index.esm.js",
@@ -45,6 +45,7 @@
45
45
  flex-direction: column;
46
46
  padding: 20px;
47
47
  border-radius: 25px;
48
+ overflow: hidden;
48
49
  }
49
50
  }
50
51
 
@@ -53,7 +54,6 @@
53
54
  .will-filter-bar-container {
54
55
  background-color: var(--will-white);
55
56
  min-height: 100px;
56
- padding: 20px;
57
57
  position: absolute;
58
58
  top: 125px;
59
59
  z-index: 111;
@@ -59,21 +59,15 @@ export const Main: Story = {
59
59
  tabs: [
60
60
  {
61
61
  path: '/rooms',
62
+ label: 'Rooms',
62
63
  default: true,
63
64
  order: 2,
64
- label: {
65
- en: 'Rooms',
66
- fi: 'Rooms fi',
67
- },
68
65
  },
69
66
  {
70
67
  path: '/events',
68
+ label: 'Events',
71
69
  default: false,
72
70
  order: 1,
73
- label: {
74
- en: 'Events',
75
- fi: 'Events fi',
76
- },
77
71
  },
78
72
  ],
79
73
  outerLoading: false,
@@ -83,61 +77,32 @@ export const Main: Story = {
83
77
  data: [
84
78
  {
85
79
  id: 1,
86
- label: [
87
- { content: 'Helsinki Center', locale: 'en' },
88
- { content: 'Helsinki Keskusta', locale: 'fi' },
89
- ],
90
- description: [
91
- { content: 'Main training facility in downtown', locale: 'en' },
92
- { content: 'Pääkoulutuslaitoksemme keskustassa', locale: 'fi' },
93
- ],
80
+ label: 'Helsinki Center',
81
+ description: 'Main training facility in downtown',
94
82
  imageUrl: '',
95
83
  },
96
84
  {
97
85
  id: 2,
98
- label: [
99
- { content: 'Espoo Campus', locale: 'en' },
100
- { content: 'Espoon Kampus', locale: 'fi' },
101
- ],
102
- description: [
103
- { content: 'Modern facilities with sea views', locale: 'en' },
104
- { content: 'Modernit tilat merinäköalalla', locale: 'fi' },
105
- ],
106
- imageUrl: '',
86
+ label: 'Espoo Campus',
87
+ description: 'Modern facilities with sea views',
88
+ imageUrl: null,
107
89
  },
108
90
  {
109
91
  id: 3,
110
- label: [
111
- { content: 'Tampere Resort', locale: 'en' },
112
- { content: 'Tampereen Lomakeskus', locale: 'fi' },
113
- ],
114
- description: [
115
- { content: 'Lakeside retreat with full amenities', locale: 'en' },
116
- { content: 'Järvenrannalla sijaitseva lomakeskus', locale: 'fi' },
117
- ],
118
- imageUrl: '',
92
+ label: 'Tampere Resort',
93
+ description: 'Lakeside retreat with full amenities',
94
+ imageUrl: null,
119
95
  },
120
96
  {
121
97
  id: 4,
122
- label: [
123
- { content: 'Turku Harbor', locale: 'en' },
124
- { content: 'Turun Satama', locale: 'fi' },
125
- ],
98
+ label: 'Turku Harbor',
99
+ description: 'Modern facilities with sea views',
126
100
  imageUrl: null,
127
101
  },
128
102
  {
129
103
  id: 5,
130
- label: [
131
- { content: 'Oulu North', locale: 'en' },
132
- { content: 'Oulun Pohjoinen', locale: 'fi' },
133
- ],
134
- description: [
135
- {
136
- content: 'Northern location with winter activities',
137
- locale: 'en',
138
- },
139
- { content: 'Pohjoinen kohde talviaktiviteeteilla', locale: 'fi' },
140
- ],
104
+ label: 'Oulu North',
105
+ description: 'Northern location with winter activities',
141
106
  imageUrl: null,
142
107
  },
143
108
  ],
@@ -1,4 +1,10 @@
1
- import React, { useEffect, useRef } from 'react'
1
+ import {
2
+ CSSProperties,
3
+ MutableRefObject,
4
+ useEffect,
5
+ useRef,
6
+ useState,
7
+ } from 'react'
2
8
  import { useTranslation } from 'react-i18next'
3
9
  import { FaSearch } from 'react-icons/fa'
4
10
 
@@ -12,7 +18,7 @@ import {
12
18
  import { SubmitButton } from '../../core/components'
13
19
  import { parseDates } from '../../core/components/calendar/utils'
14
20
 
15
- import { parseGuests, parseLocations } from './utils'
21
+ import { parseGuests, parseLocations, calculateDropdownPosition } from './utils'
16
22
  import { FilterBarTypes, FilterSections, Pages } from './FilterBarTypes'
17
23
  import { useFilterBar, useScrollInToView } from './hooks'
18
24
  import {
@@ -51,6 +57,10 @@ export default function FilterBar({
51
57
  const datesButtonRef = useRef<HTMLButtonElement>(null)
52
58
  const guestsButtonRef = useRef<HTMLButtonElement>(null)
53
59
  const previouslyFocusedButtonRef = useRef<HTMLButtonElement | null>(null)
60
+ const headerRef = useRef<HTMLDivElement>(null)
61
+
62
+ // Dropdown positioning
63
+ const [dropdownStyle, setDropdownStyle] = useState<CSSProperties>({})
54
64
 
55
65
  // Filters
56
66
  const {
@@ -83,6 +93,22 @@ export default function FilterBar({
83
93
  // Handle close filter section
84
94
  const { filtersRef } = useCloseFilterSection({ handleSelectedFilter })
85
95
 
96
+ // Enhanced handleSelectedFilter with positioning
97
+ const handleSelectedFilterWithPosition = (filter: FilterSections | false) => {
98
+ if (filter) {
99
+ const position = calculateDropdownPosition({
100
+ filterSection: filter,
101
+ headerRef,
102
+ locationsButtonRef,
103
+ datesButtonRef,
104
+ guestsButtonRef,
105
+ isMobile,
106
+ })
107
+ setDropdownStyle(position)
108
+ }
109
+ handleSelectedFilter(filter)
110
+ }
111
+
86
112
  // Store previously focused button and restore focus when closing
87
113
  useEffect(() => {
88
114
  if (!selectedFilter && previouslyFocusedButtonRef.current) {
@@ -105,7 +131,6 @@ export default function FilterBar({
105
131
  })
106
132
  const parsedLocations = parseLocations({
107
133
  selectedLocations,
108
- language,
109
134
  locationsPlaceholder: t('locations.placeholder'),
110
135
  locationsSelectedLabel: t('locations.selected'),
111
136
  })
@@ -122,11 +147,7 @@ export default function FilterBar({
122
147
  .map((tab, idx) => (
123
148
  <TabButton
124
149
  key={`tab-${idx}`}
125
- label={
126
- tab.label && language
127
- ? tab.label[language]
128
- : t(`tabs.${tab.path.substring(1)}`)
129
- }
150
+ label={tab.label || t(`tabs.${tab.path.substring(1)}`)}
130
151
  onClick={() => {
131
152
  setSelectedPath(tab.path)
132
153
  handleResetFilters()
@@ -141,7 +162,14 @@ export default function FilterBar({
141
162
 
142
163
  <div
143
164
  className={`will-filter-bar-header ${mode || 'light'}`}
144
- ref={tabs?.length === 1 ? targetFilterBarRef : null}
165
+ ref={(el) => {
166
+ ;(headerRef as MutableRefObject<HTMLDivElement | null>).current = el
167
+ if (tabs?.length === 1 && targetFilterBarRef) {
168
+ ;(
169
+ targetFilterBarRef as MutableRefObject<HTMLDivElement | null>
170
+ ).current = el
171
+ }
172
+ }}
145
173
  >
146
174
  {!!locations?.data?.length && (
147
175
  <>
@@ -151,7 +179,7 @@ export default function FilterBar({
151
179
  description={parsedLocations}
152
180
  onClick={() => {
153
181
  previouslyFocusedButtonRef.current = locationsButtonRef.current
154
- handleSelectedFilter(FilterSections.LOCATIONS)
182
+ handleSelectedFilterWithPosition(FilterSections.LOCATIONS)
155
183
  }}
156
184
  active={!!selectedLocations.length}
157
185
  disabled={locations?.disabled}
@@ -175,7 +203,7 @@ export default function FilterBar({
175
203
  }
176
204
  onClick={() => {
177
205
  previouslyFocusedButtonRef.current = datesButtonRef.current
178
- handleSelectedFilter(FilterSections.CALENDAR)
206
+ handleSelectedFilterWithPosition(FilterSections.CALENDAR)
179
207
  }}
180
208
  active={!!parsedDates}
181
209
  ariaExpanded={selectedFilter === FilterSections.CALENDAR}
@@ -192,7 +220,7 @@ export default function FilterBar({
192
220
  description={parsedGuests.content}
193
221
  onClick={() => {
194
222
  previouslyFocusedButtonRef.current = guestsButtonRef.current
195
- handleSelectedFilter(FilterSections.GUESTS)
223
+ handleSelectedFilterWithPosition(FilterSections.GUESTS)
196
224
  }}
197
225
  active={!!parsedGuests.data.total}
198
226
  ariaExpanded={selectedFilter === FilterSections.GUESTS}
@@ -212,7 +240,10 @@ export default function FilterBar({
212
240
  {selectedFilter && (
213
241
  <div
214
242
  className={`will-filter-bar-container ${mode || 'light'}`}
215
- style={(!tabs || tabs.length < 2) && !isMobile ? { top: 66 } : {}}
243
+ style={{
244
+ ...((!tabs || tabs.length < 2) && !isMobile ? { top: 66 } : {}),
245
+ ...dropdownStyle,
246
+ }}
216
247
  >
217
248
  {selectedFilter === FilterSections.CALENDAR && (
218
249
  <div id="will-dates-filter">
@@ -50,22 +50,11 @@ export enum Pages {
50
50
  SALES = '/sales',
51
51
  }
52
52
 
53
- type Translations = {
54
- en: string
55
- fi: string
56
- [key: string]: string
57
- }
58
-
59
- export type LocaleTranslation = Array<{
60
- content: string
61
- locale: string
62
- }>
63
-
64
53
  export type Tab = {
65
54
  path: string
66
55
  default?: boolean
67
56
  order: number
68
- label?: Translations
57
+ label?: string
69
58
  }
70
59
 
71
60
  export type Locations = {
@@ -76,7 +65,7 @@ export type Locations = {
76
65
 
77
66
  export type Location = {
78
67
  id: number
79
- label: LocaleTranslation
80
- description?: LocaleTranslation | null
68
+ label: string
69
+ description?: string
81
70
  imageUrl?: string | null
82
71
  }
@@ -6,7 +6,6 @@
6
6
  padding: 8px 16px;
7
7
  cursor: pointer;
8
8
  user-select: none;
9
- border-radius: 8px;
10
9
  min-height: 40px;
11
10
  }
12
11
 
@@ -2,6 +2,7 @@
2
2
  display: flex;
3
3
  justify-content: space-between;
4
4
  align-items: center;
5
+ padding: 16px;
5
6
  }
6
7
 
7
8
  .will-filter-section-title {
@@ -0,0 +1,3 @@
1
+ .will-dates-filter-container {
2
+ padding: 0 16px;
3
+ }
@@ -7,6 +7,8 @@ import { CloseButton } from '../buttons'
7
7
  import { FilterSectionHeader } from '../common/FilterSectionHeader'
8
8
  import { forwardRef } from 'react'
9
9
 
10
+ import './Dates.css'
11
+
10
12
  type Props = {
11
13
  ref: React.RefObject<HTMLDivElement>
12
14
  onClose?: () => void
@@ -7,7 +7,7 @@
7
7
  flex-direction: column;
8
8
  min-width: 400px;
9
9
  gap: 20px;
10
- margin-top: 20px;
10
+ padding: 16px;
11
11
  }
12
12
 
13
13
  @media (max-width: 960px) {
@@ -7,7 +7,7 @@
7
7
  gap: 10px;
8
8
  flex-direction: column;
9
9
  min-width: 400px;
10
- margin-top: 20px;
10
+ padding: 16px 0;
11
11
  }
12
12
 
13
13
  @media (max-width: 960px) {
@@ -6,7 +6,6 @@ import { ImageCard } from '../cards/image-card/ImageCard'
6
6
  import { Location } from '../../FilterBarTypes'
7
7
  import { FilterSectionHeader } from '../common/FilterSectionHeader'
8
8
  import { CloseButton } from '../../../../core/components'
9
- import { getLocalizedContent } from '../../utils'
10
9
 
11
10
  type Props = {
12
11
  locations?: Location[]
@@ -72,40 +71,21 @@ export const Locations = forwardRef<HTMLDivElement, Props>(
72
71
 
73
72
  <div className="will-locations-filter-container">
74
73
  {!!(locations?.length && language) &&
75
- locations
76
- .filter((location) => {
77
- const label = getLocalizedContent({
78
- contents: location.label,
79
- locale: language,
80
- })
81
- return !!label
82
- })
83
- .map((location, index) => {
84
- const label = getLocalizedContent({
85
- contents: location.label,
86
- locale: language,
87
- })
88
- const description = location.description
89
- ? getLocalizedContent({
90
- contents: location.description,
91
- locale: language,
92
- })
93
- : null
94
-
95
- return (
96
- <ImageCard
97
- key={location.id}
98
- ref={index === 0 ? firstCardRef : null}
99
- title={label}
100
- description={description}
101
- imageUrl={location.imageUrl}
102
- isSelected={selectedLocations.some(
103
- (loc) => loc.id === location.id
104
- )}
105
- onClick={() => handleLocationClick(location)}
106
- />
107
- )
108
- })}
74
+ locations.map((location, index) => {
75
+ return (
76
+ <ImageCard
77
+ key={location.id}
78
+ ref={index === 0 ? firstCardRef : null}
79
+ title={location.label}
80
+ description={location.description}
81
+ imageUrl={location.imageUrl}
82
+ isSelected={selectedLocations.some(
83
+ (loc) => loc.id === location.id
84
+ )}
85
+ onClick={() => handleLocationClick(location)}
86
+ />
87
+ )
88
+ })}
109
89
  </div>
110
90
  </div>
111
91
  )
@@ -0,0 +1,106 @@
1
+ import { CSSProperties, RefObject } from 'react'
2
+ import { FilterSections } from '../FilterBarTypes'
3
+
4
+ type CalculateDropdownPositionParams = {
5
+ filterSection: FilterSections
6
+ headerRef: RefObject<HTMLDivElement>
7
+ locationsButtonRef: RefObject<HTMLButtonElement>
8
+ datesButtonRef: RefObject<HTMLButtonElement>
9
+ guestsButtonRef: RefObject<HTMLButtonElement>
10
+ isMobile: boolean
11
+ }
12
+
13
+ export const calculateDropdownPosition = ({
14
+ filterSection,
15
+ headerRef,
16
+ locationsButtonRef,
17
+ datesButtonRef,
18
+ guestsButtonRef,
19
+ isMobile,
20
+ }: CalculateDropdownPositionParams): CSSProperties => {
21
+ // On mobile, don't apply any positioning - let CSS handle it naturally
22
+ // Dropdowns will start from leftmost point with position: relative
23
+ if (isMobile) {
24
+ return {}
25
+ }
26
+
27
+ if (!headerRef.current) return {}
28
+
29
+ const containerRect = headerRef.current.getBoundingClientRect()
30
+ const containerLeft = 0
31
+
32
+ switch (filterSection) {
33
+ case FilterSections.LOCATIONS:
34
+ // Locations: Start from beginning, hug content
35
+ if (locationsButtonRef.current) {
36
+ const buttonRect = locationsButtonRef.current.getBoundingClientRect()
37
+ const relativeLeft = buttonRect.left - containerRect.left
38
+ return {
39
+ left: relativeLeft,
40
+ right: 'auto',
41
+ width: 'auto',
42
+ }
43
+ }
44
+ break
45
+
46
+ case FilterSections.CALENDAR:
47
+ // Calendar: Two months side-by-side, needs ~650-700px
48
+ // Start from dates button, but push left if not enough space
49
+ if (datesButtonRef.current) {
50
+ const buttonRect = datesButtonRef.current.getBoundingClientRect()
51
+ const relativeLeft = buttonRect.left - containerRect.left
52
+ const availableWidth = containerRect.width - relativeLeft
53
+ const calendarMinWidth = 650
54
+
55
+ if (availableWidth < calendarMinWidth) {
56
+ // Not enough space, align to the right edge
57
+ return {
58
+ left: 'auto',
59
+ right: containerLeft,
60
+ width: 'auto',
61
+ maxWidth: `${containerRect.width}px`,
62
+ }
63
+ } else {
64
+ // Enough space, start from dates button
65
+ return {
66
+ left: relativeLeft,
67
+ right: 'auto',
68
+ width: 'auto',
69
+ }
70
+ }
71
+ }
72
+ break
73
+
74
+ case FilterSections.GUESTS:
75
+ // Guests: Start from guests button, push left if not enough space
76
+ if (guestsButtonRef.current) {
77
+ const buttonRect = guestsButtonRef.current.getBoundingClientRect()
78
+ const relativeLeft = buttonRect.left - containerRect.left
79
+ const availableWidth = containerRect.width - relativeLeft
80
+ const dropdownMinWidth = 350
81
+
82
+ if (availableWidth < dropdownMinWidth) {
83
+ // Not enough space, align to the right
84
+ return {
85
+ left: 'auto',
86
+ right: containerLeft,
87
+ width: 'auto',
88
+ maxWidth: `${containerRect.width}px`,
89
+ }
90
+ } else {
91
+ // Enough space, start from button
92
+ return {
93
+ left: relativeLeft,
94
+ right: 'auto',
95
+ width: 'auto',
96
+ }
97
+ }
98
+ }
99
+ break
100
+
101
+ default:
102
+ return {}
103
+ }
104
+
105
+ return {}
106
+ }
@@ -1,3 +1,3 @@
1
1
  export { parseGuests } from './parseGuests'
2
2
  export { parseLocations } from './parseLocations'
3
- export { getLocalizedContent } from './getLocalizedContent'
3
+ export { calculateDropdownPosition } from './calculateDropdownPosition'
@@ -1,16 +1,14 @@
1
1
  import { Location } from '../FilterBarTypes'
2
- import { getLocalizedContent } from './getLocalizedContent'
3
2
 
4
3
  type Props = {
5
4
  selectedLocations: Location[]
6
- language: string
7
5
  locationsPlaceholder: string
8
6
  locationsSelectedLabel?: string
9
7
  }
10
8
 
11
9
  export const parseLocations = ({
12
10
  selectedLocations,
13
- language,
11
+
14
12
  locationsPlaceholder,
15
13
  locationsSelectedLabel = 'locations',
16
14
  }: Props) => {
@@ -19,10 +17,8 @@ export const parseLocations = ({
19
17
  }
20
18
 
21
19
  if (selectedLocations.length === 1) {
22
- const translation = getLocalizedContent({
23
- contents: selectedLocations[0].label,
24
- locale: language,
25
- })
20
+ const translation = selectedLocations[0].label
21
+
26
22
  if (!translation) {
27
23
  return locationsPlaceholder
28
24
  }
@@ -1,21 +0,0 @@
1
- import { LocaleTranslation } from '../FilterBarTypes'
2
-
3
- type Props = {
4
- contents: LocaleTranslation
5
- locale: string
6
- fallbackLocale?: string
7
- }
8
-
9
- export const getLocalizedContent = ({
10
- contents,
11
- locale,
12
- fallbackLocale = 'en',
13
- }: Props): string | undefined => {
14
- const preferred = contents.find((content) => content.locale === locale)
15
- if (preferred) return preferred.content
16
-
17
- const fallback = contents.find((content) => content.locale === fallbackLocale)
18
- if (fallback) return fallback.content
19
-
20
- return contents[0]?.content
21
- }